Sommaire

BONJOUR
Musique "Invention BWV784" J.S. BACH 1685-1750

Tutoriel et Help

Le CSharp C ♯


CSharp tutoriel

Le C Sharp

Un article de Wikipédia, l'encyclopédie libre.

Le C♯ (prononcé [siː.ʃɑːp]) est un langage de programmation orienté objet à typage fort, créé par la société Microsoft, et notamment un de ses employés, Anders Hejlsberg, le créateur du langage Delphi.

Il a été créé afin que la plate-forme Microsoft .NET soit dotée d'un langage permettant d'utiliser toutes ses capacités. Il est très proche du Java dont il reprend la syntaxe générale ainsi que les concepts (la syntaxe reste cependant relativement semblable à celle de langages tels que le C++ et le C). Un ajout notable au C♯ est la possibilité de surcharge des opérateurs, inspirée du C++. Toutefois, l'implémentation de la redéfinition est plus proche de celle du Pascal Objet.

  • 1 Plate-forme d'exécution
  • 2 Capacités du langage
  • 3 Différences entre Java et C♯
  • 4 Standardisation
  • 5 Le langage
  • 6 Notes et références
  • 7 Liens externes
  • 8 Quelques Livres

Plate-forme d'exécution

  • La plate-forme Microsoft .NET (possibilité de mixage avec des modules d'autres langages)
  • Des implémentations libres de ce langage et de sa plate-forme d'exécution sont en cours de finalisation, comme le projet Mono maintenu par Novell, ou dotGNU maintenu par la Free Software Foundation. L'idée fondatrice de ces projets est qu'une application en C♯ puisse s'exécuter sans modification sur une plate-forme propriétaire comme Windows ou libre comme Linux.

Capacités du langage C♯

Le C♯ est, d’une certaine manière, le langage de programmation qui reflète le mieux l’architecture Microsoft .NET qui fait fonctionner toutes les applications .NET, et en est par conséquent extrêmement dépendant. Les types natifs correspondent à ceux de .NET, les objets sont automatiquement nettoyés par un ramasse-miettes (garbage collector en anglais), et beaucoup de mécanismes comme les classes, interfaces, délégués, exceptions, ne sont que des moyens explicites d’exploiter les fonctionnalités de la bibliothèque .NET. Pour achever de marquer cette dépendance, le CLR (Common Language Runtime) est obligatoire pour exécuter des applications écrites en C♯, comme l’est la JVM (Java Virtual Machine ou Machine virtuelle Java) pour des applications Java.

Le langage compte un certain nombre de changements par rapport au C/C++ ; On notera particulièrement les points suivants :

  • La manipulation directe de pointeurs ne peut se faire qu’au sein d’un code marqué unsafe, et seuls les programmes avec les permissions appropriées peuvent exécuter des blocs de code unsafe. La plupart des manipulations de pointeurs se font via des références sécurisées, dont l’adresse ne peut être directement modifiée, et la plupart des opérations de pointeurs et d’allocations sont contrôlées contre les dépassements de mémoire. Les pointeurs ne peuvent pointer que sur des types de valeurs, les types objets, manipulés par le ramasse-miettes, ne pouvant qu’être référencés.
  • Les objets ne peuvent pas être explicitement détruits. Le ramasse-miettes s’occupe de libérer la mémoire lorsqu’il n’existe plus aucune référence pointant sur un objet. Toutefois, pour les objets gérant des types non managés, il est possible d’implémenter l’interface IDisposable pour spécifier des traitements à effectuer au moment de la libération de la ressource.
  • L’héritage multiple de classes est interdit, mais une classe peut implémenter un nombre illimité d’interfaces, et une interface peut hériter de plusieurs interfaces.
  • Le C♯ est beaucoup plus typé que le C++ ; les seules conversions implicites sont celles entre les différentes gammes d’entiers et celles d’un type dérivé à un type parent. Aucune conversion implicite n’a lieu entre booléens et entiers, entre membres d’énumération et entiers, ni de pointeurs sur un type void (quoique pour ce dernier point l’utilisation de références sur le type Object permette d’obtenir le même effet). Les conversions définies par l’utilisateur peuvent être définies comme implicites ou explicites.
  • La syntaxe pour la déclaration des tableaux n’est pas la même : int[] a = new int[5] remplace int a[5]. Car il s'agit d'une allocation dynamique, int[] a étant la déclaration d'une référence (nulle si non initialisée). L'allocation manuelle d'un tableau sur la pile reste cependant possible avec le mot clé stackalloc[1].
  • Les membres d’une énumération sont rassemblés dans leur propre espace de noms.
  • Le C♯ ne gère pas les templates, mais cette fonctionnalité a été remplacée par les types génériques apparus avec C♯ 2.0.
  • Les propriétés ont été introduites, et proposent une syntaxe spécifique pour l’accès aux données membres (ainsi que la facilitation de l’accès simultané par plusieurs threads).
  • La réflexion totale des types est disponible.
  • Les délégués, qui sont des listes de pointeurs sur fonctions, sont utilisés notamment pour la programmation événementielle.

Capacités introduites avec C♯ 2.0

Microsoft mit à disposition du public en octobre 2005, après une longue période de beta-tests, la version 2.0 de la bibliothèque .NET, accompagnée d’une nouvelle version de la quasi-totalité des outils associés. C♯ ne fait pas exception à la règle et sort donc en version 2.0, avec les ajouts suivants :

  • Les classes partielles, permettant de répartir l’implémentation d’une classe sur plusieurs fichiers.
  • Les types génériques, qui ne sont pas une simple copie des templates C++. Par exemple, on trouvera dans les génériques C♯ la restriction de types (pour spécifier les types utilisables dans une généralisation). Par contre, il est impossible d’utiliser des expressions comme paramètres pour la généralisation.
  • Un nouvel itérateur qui permet l’utilisation de coroutines via le mot-clé yield, équivalent du yield que l’on trouve en Python.
  • Les méthodes anonymes avec des règles de fermeture configurables.
  • Les types « nullables », c'est-à-dire la possibilité de spécifier qu’un type de valeur peut être nul. Ceux-ci sont déclarés avec le caractère point d'interrogation « ? » suivant le nom du type, comme ceci : int? i = null;.
  • Le nouvel opérateur double point d'interrogation « ?? » utilise deux opérandes et retourne le premier non nul. Il a été introduit pour spécifier une valeur par défaut pour les types « nullables ».

À titre de référence, les spécifications complètes des nouveautés introduites dans la version 2.0 sont disponibles dans les liens externes.

Anders Hejlsberg, le père de Delphi, s’est exprimé sur l’implémentation des génériques dans C♯, Java et C++ dans cette interview (en).

La fonctionnalité des types nullables fut fixée quelques semaines seulement avant la sortie publique de la version 2.0, car il a été mis en lumière que si la valeur de la variable était bien nulle, cette variable n’était pas nulle au sens traditionnel du terme, c'est-à-dire qu’il ne s’agit pas d’une référence vide. Ainsi, la conversion d’un type primitif de valeur nulle en objet donnait une référence non nulle vers une valeur nulle. Il fallut donc, pour corriger ce problème, corriger le noyau du CLR et effectuer de nombreuses vérifications et corrections sur tous les produits de la gamme .NET 2.0 (Visual Studio 2005, SQL Server 2005, C♯ et VB.NET).

Capacités introduites dans C♯ 3.0

Le C♯ 3.0 fut présenté au salon PDC 2005. La version finale est disponible depuis le 19 novembre 2007 au téléchargement sur le site de Microsoft (en) . Les principales nouveautés sont les suivantes :

  • L’ajout des mots-clefs select, from et where pour permettre la formation et l’exécution de requêtes SQL, XML, ou directement sur des collections. Cette fonctionnalité fait partie du programme Language Integrated Query (LINQ) (en).
  • Nouvelle
     possibilité d’initialisation d'un objet : À la
     place de Client c = new Client(); c.Nom =
     "Dupont";, on peut utiliser Client c = new Client{ Nom = "Dupont"
     };
  • Expressions lambda : ListeTrucs.Where( delegate(Truc x) { return x.Size > 10; } ); devient ListeTrucs.Where( x => x.Size > 10 );
  • Inférence du type des variables locales : string s = "Dupont" peut être remplacé par var s = "Dupont"
  • Introduction des types anonymes : var x = new { Nom = "Dupont" } peut être utilisé à la place de class __anonymous { private string _nom; public string Nom { get { return _nom; } set { _nom = value; } } } __anonymous x = new __anonymous(); x.Nom = "Dupont";
  • Les arbres d'expressions (expression trees) : permettent la compilation du code sous formes d'arbres d'objets facilement analysables et manipulables.
  • Méthodes étendues : permet d'ajouter des méthodes à une classe en y ajoutant un premier paramètre this.

Une présentation du C♯ 3.0 et de LINQ peut être trouvée sur la page du centre de développement de .NET Framework.

Le code compilé en C♯ 3.0 est entièrement compatible avec celui du 2.0, étant donné que les améliorations apportées ne sont que purement syntaxiques ou ne consistent qu’en des raccourcis compensés au moment de la compilation. Les nouveautés introduites dans les bibliothèques de la version 3.5 (LINQ…) ne sont cependant pas utilisables avec les versions précédentes de C♯.

Cette version exige Windows XP ou une version supérieure (Vista ou Windows 7). Elle n'est pas disponible pour Windows 2000.

C♯ 4.0

La version 4 du langage apporte plusieurs nouveautés :

  • le typage dynamique des variables à l'aide du mot clé dynamic ;
  • les arguments nommés et facultatifs[2] ;
  • le support de la covariance et de la contravariance pour les interfaces et les délégués génériques.

Le framework .NET 4.0 est sorti le 12 avril 2010, accompagné de Visual Studio 2010[3]. Il propose entre autres :

  • la nouvelle bibliothèque parallèle : Task Parallel Library ;
  • une version optimisée de la plate-forme entité d'accès aux bases de données (Entity Framework) via l'utilisation de LINQ ;
  • la version parallèle de LINQ appelée PLINQ.

C♯ 5.0

La version 5 du langage permet de programmer plus simplement des programmes asynchrones grâce à l'ajout des mots clés async et await.

Le comportement des closures dans la boucle foreach a été modifié. Il n'est désormais plus nécessaire d'introduire une variable locale dans une boucle foreach pour éviter les problèmes de closure[4].

À noter également les informations relatives à l'appelant[5] permettent de connaître le nom de la méthode qui a appelé une propriété.

Différences entre Java et C♯

Bien que le C♯ soit similaire à Java, il existe des différences notables, par exemple :

  • Java n'autorise pas la surcharge des opérateurs ;
  • Java n'a pas de mode unsafe permettant l'arithmétique de pointeurs ;
  • Java a des exceptions vérifiées, alors que les exceptions du C♯ ne sont pas vérifiées, comme en C++ ;
  • Java permet la génération automatique de la documentation HTML à partir des fichiers sources à l'aide des descriptions Javadoc-syntax, tandis que le C♯ utilise des descriptions basées sur le XML ;
  • Java n'a pas de langage préprocesseur
  • Java n'a pas de mode unsafe
  • C♯ supporte les indexeurs, les méthodes déléguées, et les événements (là où Java se contente du patron de conception Observateur) ;
  • C♯ ne supporte pas les classes anonymes ;
  • C♯ ne supporte que les classes internes statiques ;
  • C♯ supporte les structures en plus des classes (les structures sont des types valeur : on stocke le contenu et non l'adresse) ;
  • C♯ utilise une syntaxe intégrée au langage (DllImport) et portable pour appeler une bibliothèque native, tandis que Java utilise Java Native Interface ;
  • C♯ supporte la généricité, et la machine .NET a été modifiée pour permettre cela (Java la supporte également, mais son implémentation a été réalisée dans le compilateur javac sans changer le bytecode Java). Plus de détails sur l'aspect théorique de cette réalisation peuvent être trouvés dans la référence[6], diapositives 70 à 89.

Standardisation

Le C♯ a été normalisé par l'ECMA

(ECMA-334) en décembre 2001 et par l'ISO/CEI (ISO/CEI 23270) en 2003.

Les modifications survenues dans la Version 2.0 ont été normalisées par l'ECMA

(ECMA-334) en juin 2006 et par l'ISO/CEI (ISO/IEC 23270:2006) en septembre 2006.

Microsoft a ouvert le code source de certaines bibliothèques utilisées par le C♯ en octobre 2007 sous la licence Microsoft Reference License (MS-RL).

Nom du langage

C♯ est une note de musique dans le système de notation musical américain (cette note correspond à « Do dièse » dans la notation française). Le symbole ♯ signifie en musique que la note doit être augmentée, l'idée de « C augmenté » rappelle la manière dont le langage C++ a été nommé (++ étant l'opérateur d'incrémentation).

Usuellement, un croisillon (#) est utilisé comme second caractère à la place du dièse (♯) car ce dernier est moins accessible sur le clavier et non reconnu dans certaines polices de caractères.

Le langage

Voici un exemple d'un programme Hello world typique, écrit en C♯ :

using System;
class HelloWorld
{
public static void
Main()
{
Console.WriteLine("Hello World!");
}
}

Gestion des exceptions

C♯ possède les instructions try et catch permettant de gérer les exceptions (comportement non attendu des instructions du programme), similaires dans la syntaxe à celles du C++.
Exemple de code tentant de créer un fichier « document.txt » sur le serveur « Toto » :

try
{
// Tentative de création du fichier 'document.txt' sur le serveur 'Toto'
File.Create(@"\\toto\document.txt");
}
catch
{
// Impossible de contacter le server Toto
MessageBox.Show("L'application n'arrive pas à créer le fichier 'document.txt' sur le serveur 'Toto' !", "Alerte");
}

Notez l'utilisation d'une chaîne de caractères verbatim : le caractère arobase précède le guillemet donc l'anti-slash n'est pas doublé. Ce genre de chaîne de caractères est pratique pour les chemins sous Windows.

Dans cet exemple, la fonction File.Create retourne un flux (FileStream), ou elle peut lancer une exception si une erreur s'est produite (problème de connexion par exemple).

Dans cet exemple, aucune information sur l'exception n'est obtenue : on cherche juste à savoir si le programme ne s'est pas comporté normalement, auquel cas on arrive dans le bloc catch.

À l'instar de C++ qui a un type d'exception de base (class exception dans l'en-tête <exception>) et dont les autres exceptions héritent, toute exception C♯ est héritée (ou une instance) du type System.Exception. Ainsi, si on cherche à savoir ce qui s'est passé, une solution simple reste d'obtenir une référence vers l'exception de la manière suivante :

try
{
// Tentative de création du fichier 'document.txt' sur le serveur 'Toto'
File.Create(@"\\toto\document.txt");
}
catch(Exception err)
{
// Impossible de contacter le server Toto
MessageBox.Show("L'application n'arrive pas à créer le fichier 'document.txt' sur le serveur 'Toto' ! Erreur:" + err.Message, "Alerte");
}

Ainsi, une information complète sera retournée, décrivant la nature de l'exception qui s'est produite.

En fonction des fonctions appelées, le framework .NET fournit la liste des exceptions que l'appel est susceptible de retourner en cas d'erreur. Dans le cas de la fonction 'Create', voici la liste des exceptions possibles :

  • System.UnauthorizedAccessException
  • System.ArgumentException
  • System.ArgumentNullException
  • System.IO.PathTooLongException
  • System.IO.DirectoryNotFoundException
  • System.IO.IOException
  • System.NotSupportedException
try
{
// Tentative de création du fichier 'document.txt' sur le serveur 'Toto'
File.Create(@"\\toto\document.txt");
}
catch(System.ArgumentException ArgumentErr)
{
// L'argument n'est pas valable. Le nom de fichier 'document.txt' n'est pas valable
MessageBox.Show("L'argument n'est pas valable. Le nom de fichier 'document.txt' n'est pas valable ! Erreur:" + ArgumentErr.Message, "Alerte");
}
catch(Exception err)
{
// Impossible de contacter le server Toto
MessageBox.Show("L'application n'arrive pas à créer le fichier 'document.txt' sur le serveur 'Toto' ! Erreur:" + err.Message, "Alerte");
}

De la même manière qu'en C++, l'envoi d'une exception se fait avec le mot-clef throw :

public uint Divide(uint num, uint div) //Fonction de division dans N
{
if(div == 0)
throw new Exception ("Division par 0 !");
if(num < div)
throw new Exception ("num est strictement inférieur à div : le résultat de la division ne sera pas un entier naturel !");
return num / div;
}
//Code ailleurs :
try
{
Divide(12, 6); //Retourne 2, aucune exception
Divide(10, 20); //Exception, cette division ne donne aucun résultat dans N
Divide(10, 0); //Exception, division par zéro impossible
}
catch (Exception e)
{
MessageBox.Show("L'erreur suivante a été retournée :\n" + e.Message, "Alerte");
}

L'envoi d'une exception (via throw) ou la levée d'une exception (dans un bloc try/catch) met immédiatement fin au bloc en cours. Ainsi, si div est nul, le code de la fonction Divide s'arrêtera à la ligne 2. De même, dans l'exemple précédent, la troisième fonction Divide ne sera jamais exécutée à cause de la levée d'une exception lors du deuxième appel à Divide. Une exception non attrapée (catchée) - c'est-à-dire que l'application n'a pas encadré le code lançant l'exception par un bloc catch approprié - met fin à l'application immédiatement sous la forme d'une exception de type UnhandledException (exception non gérée en français).

Comme en Java, il est possible d'ajouter un bloc finally pour exécuter une série d'instructions, quoi qu'il se passe (exception lancée ou non). Cela est utile pour libérer des ressources quel que soit ce qui peut se passer entre la prise de ressource et la libération. Exemple typique : lecture de fichiers

FileStream fs = new FileStream(@"C:\Fichier.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite);
// arrivé ici, le fichier est ouvert
try
{
// ... opérations de lecture / écriture pouvant lancer des exceptions ...
}
finally
{
// fermer le fichier quoi qu'il se passe :
fs.Close();
}

Notes et références

  1. ↑ (fr) Site MSDN
  2. ↑ http://msdn.microsoft.com/fr-fr/library/vstudio/dd264739(v=vs.100).aspx
  3. ↑ (en) Site MSDN.
  4. ↑ http://blogs.msdn.com/b/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx
  5. ↑ http://msdn.microsoft.com/fr-fr/library/hh534540.aspx
  6. ↑ (en) Support du cours de sous-typage du MPRI[PDF], diapositives 73 à 80.

Liens externes

Sur les autres projets Wikimedia :

· C sharp , sur Wikiversity

· C sharp , sur Wikibooks

  • (fr) Centre de Développement C♯ - Site MSDN
  • (fr) Exemple de programmation en C♯
  • (en) Page du standard ISO

Quelques Livres

  • (fr) C♯ et .NET - Gérard Leblanc - 2002 - Éditions Eyrolles - ( ISBN 2-212-11066-9 )
  • (fr) C♯ et .NET Version 2 - Gérard Leblanc - 2006 - Éditions Eyrolles - ( ISBN 2-212-11778-7 )
  • (fr) C♯ Tête la première - Andrew Stellman, Jennifer Greene - 2008 - Éditions Digit Books - ( ISBN 978-2-81500-001-7 )
  • (fr) Introduction à C♯ - Pierre-Yves Saumont, Antoine Mirecourt - 2001 - Éditions Eyrolles - ( ISBN 2-7464-0301-3 )
  • (fr) Le guide de survie C♯ - Gilles Tourreau - 2010 - Éditions Pearson Education - ( ISBN 978-2-7440-2432-0 )
  • (en) C# 2005 Programmer's Reference - Adrian Kingsley-Hughes, Kathie Kingsley-Hughes - 2006 - Édition Wrox - (ISBN 0-470-04641-4)
  • (en) C♯ Essentials, 2nd Edition - Ben Albahari, Peter Drayton, Brad Merrill - 2002 - Édition O'Reilly Media - (ISBN 0-596-00315-3)
  • (en) Professional C♯, 3rd Edition - Simon Robinson, Christian Nagel, Karli Watson, Jay Glynn, Morgan Skinner, Bill Evjen - 2004 - Édition Wrox - (ISBN 0-7645-5759-9)
v · d · m Microsoft
Bill Gates · Steve Ballmer · Satya Nadella
Systèmes d'exploitation | Windows 8 · Windows Phone 8 · Windows Server 2012
Logiciels | Office ( Word, Excel, PowerPoint, Outlook…) · Internet Explorer · Lecteur Windows Media
Applications Web | Outlook.com · Calendrier · Contacts · OneDrive
Portail, moteur de recherche | MSN · Bing
Matériel | Xbox 360 · Xbox One · Surface
Développement | Visual Studio · .NET · Visual Basic .NET · C sharp · SQL Server
Jeux | Age of Empires · Flight Simulator
Produits arrêtés | Encarta · Works · Money · Messenger

Langages de programmation
Orientés objet | ActionScript · AppleScript · C++ · C# · CoffeeScript · D · Delphi · Eiffel · Groovy · Java · JavaScript · Lisaac · Logo · Objective-C · PHP · Python · Ruby · Scala · Simula · Smalltalk · Visual Basic
Impératifs | Algol · APL · ASP · Assembleur · BASIC · BCPL · Shell Unix · C · COBOL · Natural · Forth · Fortran · Limbo · Lua · Modula-2 · NQC · NXC · OPL · Pascal · Perl · PHP · Rust · PL/I · Tcl · Windows PowerShell · WLangage
Fonctionnels | Haskell · Lisp / Common Lisp · ML / OCaml / F# · Opa · Scheme · XSLT
Déclaratifs | Clips · Prolog
Concurrents | Ada · Erlang
Conception · Codage · Tests · Optimisations

·


CSharp tutoriel 2008

Les mots clés de C# 1.0

Suite à l'article Les mots réservés du langage Java de Baptiste Wicht, il était temps de faire de même.

Au travers de cet article, vous découvrirez ce qu'est un mot clé avec une présentation de chacun des 77 mots clés du langage C#.

I. Introduction

Tout d'abord, un mot clé (ou keyword an anglais) est un mot réservé qui a une signification particulière pour le compilateur. Les mots clés ne pourront donc pas être utilisés pour identifier les variables, classes ou encore les fonctions.

Au cours de cet article, l'ensemble des mots clés de C# vous seront présentés. Vous verrez aussi comment les utiliser pour identifier des variables, méthodes voir classes contrairement à ce qui a été dit précédemment.

II. Des mots pas si réservés que ça...

Bien que déconseillé, il vous est tout à fait possible d'utiliser les mots clés pour identifier les variables, classes et autres.

II-A. Première technique - sensibilité à la casse

C# étant sensible à la casse, vous pouvez donc utiliser un mot clé en modifiant sa casse d'origine :

void While()
{
    int iF = 0;
}

II-B. Seconde technique - préfixe @

La seconde technique permet d'utiliser un mot clé en le préfixant de @ :

void @while()
{
    int @if = 0;}

III. Liste des mots clés

Voici la liste des mots clés qui seront détaillés par la suite :

abstract | as | base | bool | break
byte | case | catch | char | checked
class | const | continue | decimal | default
delegate | do | double | else | enum
event | explicit | extern | false | finally
fixed | float | for | foreach | goto
if | implicit | in | int | interface
internal | is | lock | long | namespace
new | null | object | operator | out
override | params | private | protected | public
readonly | ref | return | sbyte | sealed
short | sizeof | stackalloc | static | string
struct | switch | this | throw | true
try | typeof | uint | ulong | unchecked
unsafe | ushort | using | virtual | volatile
void | while |  |  |

IV. Description des mots clés

abstract

abstract est utilisé avec les classes, méthodes, propriétés, indexeurs et évènements. En utilisant abstract dans la déclaration d'une classe, cela signifiera que cette classe est une classe de base qui ne pourra pas être instanciée directement. Pour ce qui est des membres marqués comme abstraits ou contenus dans une classe abstraite, ils devront être implémentés dans la classe qui dérive de la classe abstraite.

abstract public class Animal
{
    abstract public string Cri();
}

public class Chien : Animal
{
    public override string Cri()
    {
        return "Waf !!!";
    }
}

static void Main()
{
    // Animal monAnimal = new Animal(); -> Impossible
    Chien monChient = new Chien();
}

Plus d'infos sur le mot clé ici.

as

as permet de vérifier qu'un objet est bien d'un certain de type. Si la condition est vérifiée un casting implicite est effectué pour récupérer l'objet sous le bon type, dans le cas contraire, null sera renvoyé.

abstract public class Animal
{
    abstract public string Cri();
}

public class Chien : Animal
{
    public override string Cri()
    {
        return "Waf !!!";
    }
}
public class Chat : Animal
{
    public override string Cri()
    {
        return "Miaouuu !!!";
    }
}

static void Main()
{
    Object[] mesAnimaux = { new Chien(), new Chat() };

    foreach (Object AnimalCourant in mesAnimaux)
    {
        Chien monChien = AnimalCourant as Chien;
        Chat monchat = AnimalCourant as Chat;

        if (monChien != null)
            Console.WriteLine("Voici le cri du chien : {0}", monChien.Cri());
        else if (monchat != null)
            Console.WriteLine("Voici le cri du chat : {0}", monchat.Cri());
        else
            Console.WriteLine("Animal inconnu !");
    }
}

Plus d'infos sur le mot clé ici.

base

base permet d'accéder depuis une classe dérivée à la classe de base.

public class Chien : Animal
{
    public override string Cri()
    {
        return "Waf !!!";
    }
}

public class Labrador : Chien
{
    public string CriLabrador()
    {
        return base.Cri();
    }
}

Plus d'infos sur le mot clé ici.

bool

bool est un alias de la structure System.Boolean. Vous pourrez donc aussi bien utiliser bool que Boolean pour déclarer une variable booléenne.

bool monBoolAlias = true;
Boolean monBooleanStructure = monBoolAlias;

Plus d'infos sur le mot clé ici.

break

break permet de sortir d'une boucle ou d'un switch.

Cas de la boucle
for (int i = 0; i < 5; i++)
{
    if (i == 2)
    {
        Console.WriteLine("On peut quitter la boucle !");
        break;
    }
}
Cas du switch
Sélectionnez
Console.WriteLine("Voulez-vous quitter ? [O pour oui - N pour non]");
string ret = Console.ReadLine();

switch (ret.ToUpper()[0])
{
    case 'O':
        Console.WriteLine("Au revoir");
        break;
    case 'N':
        Console.WriteLine("On continue alors...");
        break;
    default:
        Console.WriteLine("Commande inconnue !");
        break;
}

Plus d'infos sur le mot clé ici.

byte

byte est un alias de la structure System.Byte. Vous pourrez y stocker une valeur entière non signée codée sur 8 bits (de 0 à 255).

byte monByteAlias = 5;
Byte = monByteStructure = monByteAlias;

Plus d'infos sur le mot clé ici.

case

case représente un des aiguillages de l'instruction switch.

Console.WriteLine("Voulez-vous quitter ? [O pour oui - N pour non]");
string ret = Console.ReadLine();

switch (ret.ToUpper()[0])
{
    case 'O':
        Console.WriteLine("Au revoir");
        break;
    case 'N':
        Console.WriteLine("On continue alors...");
        break;
    default:
        Console.WriteLine("Commande inconnue !");
        break;
}

Plus d'infos sur le mot clé ici.

catch

catch permet de traiter une exception qui a été levée dans le bloc try précédent. Si les instructions dans le bloc try peuvent lever plusieurs exceptions différentes, il est possible de les intercepter en faisant suivre le bloc try par plusieurs bloc catch.

try
{
    int i = 0;
    int j = 5;

    int ret = j / i;
}
catch (DivideByZeroException exc)
{
    Console.WriteLine("Voici le message de l'erreur : {0}", exc.Message);
}

Remarque : Si vous désirez intercepter n'importe quelle exception dans un unique bloc catch, il vous suffit d'intercepter l'exception Exception qui est la classe de base de toute exception.

Plus d'infos sur le mot clé ici.

char

char est un alias de la structure System.Char. Il vous permet de déclarer un caractère unicode.

char monCharAlias = 'A';
Char monCharStructure = monCharAlias;

Plus d'infos sur le mot clé ici.

checked

Par défaut, C# ne signale aucune erreur en cas de dépassement de capacité sur des entiers. La valeur résultante sera donc erronée dans un tel cas. checked permet de générer une exception en cas de dépassement de capacité lors d'opérations arithmétiques, conversions ou encore division par 0.

int i = 0;
int j = 1;

// i = -2147483648
i = Int32.MaxValue + j;

try
{
    int k = 0;
    int l = 1;
    k = checked(Int32.MaxValue + l);
}
catch (Exception)
{
    Console.WriteLine("Dépassement de capacité !");
}

Plus d'infos sur le mot clé ici.

class

class, comme son l'indique, permet de déclarer une classe.

class maClasse
{ }

Plus d'infos sur le mot clé ici.

const

const permet de déclarer une variable en tant que constante. L'initialisation devra se faire en même temps que la déclaration car il ne sera plus possible de modifier la valeur de la variable par après.

const int maConstante = 2007;

Plus d'infos sur le mot clé ici.

continue

continue va de paire avec break. continue permet de passer à l'itération suivante dans une boucle contrairement à break qui permet de sortir de la boucle.

for (int i = 0; i < 5; i++)
{
    if (i == 1)
    {
        // On passe directement à l'itération suivante sans afficher le message
        continue;
    }

    Console.WriteLine("Numéro de l'itération : {0}", i);
}

Plus d'infos sur le mot clé ici.

decimal

decimal est un alias de la structure System.Decimal. Ce type permet d'avoir une plus grande précision que le type float. On peut y stocker une valeur de type flottante codée sur 128 bits (environ 8*10^28).

decimal monDecimalAlias = 9.1m;
Decimal monDecimalStructure = monDecimalAlias;

Plus d'infos sur le mot clé ici.

default

default a 2 significations :

Dans un bloc switch, default représente le bloc par défaut lorsque aucun aiguillage ne correspond.

switch (3)
{
    case 1: Console.WriteLine("Valeur 1"); break;
    case 2: Console.WriteLine("Valeur 2"); break;
    default: Console.WriteLine("Valeur par defaut"); break;
}

Sa seconde utilité est de pouvoir récupérer la valeur par défaut d'un type valeur ou référence. On y trouve une utilité lors de l'utilisation de générique où on sait déterminer si on a affaire à un type valeur ou référence.

public class MaClasse<T>
{
    public MaClasse(T param)
    {
        Object obj = default(T);

        if (obj == null)
            Console.WriteLine("T est un type référence !");
        else
            Console.WriteLine("T est un type valeur !");
    }
}

static void Main()
{
    MaClasse<int> test1 = new MaClasse<int>(5);
    MaClasse<string> test2 = new MaClasse<string>("Salut");
}

Plus d'infos sur le mot clé ici.

delegate

delegate permet de déclarer un type référence qui permettra d'encapsuler plusieurs méthodes nommées ou anonymes.

delegate void D();

static void MethodA()
{
    Console.WriteLine("Méthode A");
}
static void MethodB()
{
    Console.WriteLine("Méthode B");
}

static void Main()
{
    D monDelegue = new D(MethodA);
    // Affiche :
    // Méthode A
    monDelegue();

    monDelegue += new D(MethodB);
    // Affiche :
    // Méthode A
    // Méthode B
    monDelegue();

    monDelegue -= new D(MethodA);
    // Affiche :
    // Méthode B
    monDelegue();

    D monDelegueAnonyme = delegate { Console.WriteLine("Ceci est une méthode anonyme !"); };
    // Affiche :
    // Ceci est une méthode anonyme !
    monDelegueAnonyme();
}

Plus d'infos sur le mot clé ici.

do

do permet d'exécuter en boucle un bloc d'instructions tant que la condition renvoie vrai. A noter que la condition ne sera vérifiée qu'après avoir exécuté une première fois le bloc d'instruction.

static void Main()
{
    int i = 0;
    do
    {
        i++;
    } while (i < 5);
}

Plus d'infos sur le mot clé ici.

double

double est un alias de la structure System.Double. Vous pourrez stocker dans ce type une valeur à virgule flottante codée sur 64 bits (de 4.9*10^-324 à 1.8*10^308).

double monDoubleAlias = 13.7;
Double = monDoubleStructure = monDoubleAlias;

Plus d'infos sur le mot clé ici.

else

else va de paire avec if. Si la condition du if n'est pas vérifiée, on passera au bloc else.

Boolean valeur = true;
if (valeur == true)
{
    Console.WriteLine("La condition est vraie");
}
else
{
    Console.WriteLine("La condition est fausse");
}

Plus d'infos sur le mot clé ici.

enum

enum permet de déclarer une énumération qui représente un ensemble fini de constantes nommées.

enum Jours
{
    Lundi,
    Mardi,
    Mercredi,
    Jeudi,
    Vendredi,
    Samedi,
    Dimanche
}

Plus d'infos sur le mot clé ici.

event

event permet de déclarer un évènement. Les évènements permettent de notifier des occurrences qu'un état donné a été modifié (clique sur un bouton par exemple).

delegate void TimeEventHandler();

class TestTime
{
    public event TimeEventHandler OnTime;

    public void time()
    {
        OnTime();
    }
}

static class Program
{
    static void Main()
    {
        TestTime monTime = new TestTime();

        // On s'abonne à l'évènement
        monTime.OnTime += new TimeEventHandler(monTime_OnTime);

        // Notification
        monTime.time();
    }

    /// <summary>
    /// Handler de l'évènement onTime
    /// </summary>
    static void monTime_OnTime()
    {
        MessageBox.Show("Evènement !");
    }
}

Plus d'infos sur le mot clé ici.

explicit

explicit permet de déclarer un opérateur explicite de conversion.

public class Franc
{
    public Double Argent;

    public Franc(Double param_Argent)
    {
        Argent = param_Argent;
    }
}

public class Euro
{
    public Double Argent;

    public Euro(Double param_Argent)
    {
        Argent = param_Argent;
    }

    public static explicit operator Euro(Franc param_Franc)
    {
        return new Euro(param_Franc.Argent / 6.55957);
    }
}

static class Program
{
    static void Main()
    {
        Franc mesFranc = new Franc(1000);
        Euro ConversionEuro = (Euro)mesFranc;

        Console.WriteLine("{0} FF = {1} €", mesFranc.Argent, ConversionEuro.Argent);
    }
}

Plus d'infos sur le mot clé ici.

extern

extern permet de déclarer une méthode comme étant implémentée en externe. C'est-à-dire dans une DLL non managée. extern sera le plus souvent utilisé avec l'attribut DllImport.

static class Program
{
    /// <summary>
    /// Permet d'afficher un MessageBox
    /// </summary>
    [DllImport("User32.dll")]
    public static extern int MessageBox(int h, string m, string c, int type);

    static void Main()
    {
        MessageBox(0, "Mon texte", "Mon titre", 0);
    }
}

Plus d'infos sur le mot clé ici.

false

false est un opérateur représentant la valeur faux d'un bool (ou Boolean).

bool monBool = false;

Plus d'infos sur le mot clé ici.

finally

finally est le troisième bloc possible avec un try/catch. Il sera toujours exécuté peu importe ce qu'il arrive : en quittant le bloc try (normalement ou avec return) ou après le bloc catch suite à une exception.

Le bloc finally est la plupart du temps (pour ne pas dire tout le temps) utilisé pour libérer les ressources allouées dans le bloc try.

static void Main()
{
    ArrayList monTableau = new ArrayList();
    monTableau.Add("5");
    monTableau.Add("9");
    monTableau.Add("s");

    try
    {
        for (int i = 0; i < monTableau.Count; i++)
        {
            int monInt = Convert.ToInt32(monTableau[i]);
        }
    }
    catch (Exception)
    {
        Console.WriteLine("Il y a eu une exception !");
    }
    finally
    {
        monTableau = null;
    }
}

Plus d'infos sur le mot clé ici.

fixed

fixed est utilisé dans les contexte unsafe, c'est à dire avec utilisation de pointeurs. Dotnet peut à tout moment modifier l'emplacement en mémoire de telle ou telle autre variable ce qui pourrait poser problème si on désire parcourir un tableau à partir de l'adresse de la première cellule. fixed va permettre d'empêcher que le Garbage Collector déplace la variable dans la mémoire.

static class Program
{
    unsafe static void Main()
    {
        int[] tab = new int[2007];

        // p pointe sur la première case de tab
        fixed (int* p = tab)
        {
            Random random = new Random();
            for (int i = 0; i < tab.Length; i++)
                p[i] = random.Next();
        }
    }
}

Plus d'infos sur le mot clé ici.

float

float est un alias de la structure System.Single. Vous pourrez stocker dans ce type une valeur à virgule flottante codée sur 32 bits (de 1.4*10^-45 à 3.4*10^38).

float monFloatAlias = 13.7F;
Single monSingleStructure = monFloatAlias;

Plus d'infos sur le mot clé ici.

for

for permet de répéter une instruction ou un bloc d'instructions tant que l'expression évaluée renvoie vrai.

for (int i = 0; i < 10; i++)
{
    Console.WriteLine("{0}", i);
}

La boucle for ci-dessus se déroule comme suit :

  • i est initialisé à 0 ;
  • i est évalué : si i est bien inférieure à 10, on exécute le bloc d'instructions ;
  • Une fois le bloc exécuté, i est incrémenté de 1 ;
  • Pour être une nouvelle fois évalué ;
  • Cette séquence se répètera tant que i sera strictement inférieure à 10

Plus d'infos sur le mot clé ici.

foreach

foreach permet de parcourir une collection afin de récupérer un par un chaque élément afin d'exécuter un bloc d'instructions.

string[] stringTab = { "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche" };

foreach (string str in stringTab)
{
    Console.WriteLine("{0}", str);
}

Plus d'infos sur le mot clé ici.

goto

goto permet de sauter à une instruction étiquetée. Cette pratique est vivement déconseillée car on peut très vite se retrouver avec un code dont il est difficile de savoir ce qui sera exécuté sous telle ou telle autre condition. On appelle ça la " programmation spaghetti ".

static class Program
{
    static void Main()
    {
        int i = (new Random()).Next();

        if (i % 2 == 0)
            goto Pair;
        else
            goto Impair;

    Pair:
        Console.WriteLine("i est un nombre pair ({0})", i);
        return;

    Impair:
        Console.WriteLine("i est un nombre impair ({0})", i);
        return;

    }
}

Plus d'infos sur le mot clé ici.

if

if permet de tester une ou plusieurs condition afin d'exécuter un bloc d'instructions dans le cas où la condition est vrai. En cas de condition qui renvoie faux, on passera au test de la condition du bloc suivant (else if) ou au bloc par défaut (else) s'il y en a un.

int i = 0;

if (i == 0)
{
    Console.WriteLine("i vaut {0}", i);
}
else if (i == 1)
{
    Console.WriteLine("i vaut {0}", i);
}
else
{
    Console.WriteLine("i ne vaut ni 0, ni 1");
}

Plus d'infos sur le mot clé ici.

implicit

implicit est utilisé pour déclarer un opérateur de conversion implicite défini dans une classe.

class NombreEntier
{
    public int valeur;
    public NombreEntier(int param)
    {
        valeur = param;
    }

    // Opérateur de conversion en int
    public static implicit operator int(NombreEntier a)
    {
        return a.valeur;
    }
}
static class Program
{
    static void Main()
    {
        NombreEntier monNombre1 = new NombreEntier(5);

        // Conversion
        int monInt = (int)monNombre1;
        Console.WriteLine("monInt vaut {0}", monInt);

    }
}

Plus d'infos sur le mot clé ici.

in

in fonctionne avec le mot clé foreach qui permet de parcourir une collection. in est utilisé pour spécifier la collection dans laquelle il faudra récupérer chaque élément.

string[] stringTab = { "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche" };

foreach (string str in stringTab)
{
    Console.WriteLine("{0}", str);
}

Plus d'infos sur le mot clé ici.

int

int est un alias de la structure System.Int32. Vous pourrez stocker dans ce type une valeur entière signée codée sur 32 bits (de -2^31 à 2^31 - 1).

int monIntAlias = 2007;
Int32 monInt32Structure = monIntAlias;

Plus d'infos sur le mot clé ici.

interface

interface permet de déclarer une interface.

interface MonInterface
{
    void MethodeInterface();
}

Plus d'infos sur le mot clé ici.

internal

internal est un qualificatif qui s'applique aux classes ainsi qu'aux champs et méthodes d'une classe. Dans ce cas, seules les méthodes de la même assembly peuvent avoir accès à un champ ou une méthode qualifiée de internal.

internal class maClasse
{ }

Plus d'infos sur le mot clé ici.

is

is permet de tester si un objet est bien d'un type donné.

object[] mesObjets = { 1, "Salut", 13.7F };

foreach (object obj in mesObjets)
{
    if (obj is int)
        Console.WriteLine("int");
    else if (obj is string)
        Console.WriteLine("string");
    else
        Console.WriteLine("Type inconnu");
}

Plus d'infos sur le mot clé ici.

lock

lock permet de déclarer une section critique dans le cas d'une application multithreads en verrouillant l'accès à un objet. Tout thread qui tentera d'accéder à cette section critique et utiliser par la même occasion l'objet verrouillé se verra bloqué tant que la ressource ne sera pas libérée.

object obj = new object();

lock (obj)
{
    // Début section critique

    // instructions

    // Fin section critique
}

Plus d'infos sur le mot clé ici.

long

long est un alias de la structure System.Int64. Vous pourrez stocker dans ce type une valeur entière signée codée sur 64 bits (de -9.2*10^18 à 9.2*10^18).

long monLongAlias = 1000000000;
Int64 monInt64Structure = monLongAlias;

Plus d'infos sur le mot clé ici.

namespace

namepace sert à spécifier une portée afin d'organiser ses classes. C'est comme si on rangeait les classes dans des dossiers pour avoir une organisation qui permet de s'y retrouver facilement.

Répertoire "Opérations" dans le projet :

namespace MonNamespace.Operations
{
    class Operations
    { }
}

Avec 2 sous répertoires "Entiers" et "Reels" :

namespace MonNamespace.Operations.Entiers
{
    class Entiers
    { }
}

namespace MonNamespace.Operations.Reels
{
    class Reels
    { }
}

Le Framework Class Library (FCL) de Dotnet est organisée en namespace afin d'y avoir un accès intuitif avec des noms de namespace significatifs (comme System.xml pour les classes gérant le Xml).

Plus d'infos sur le mot clé ici.

new

new consiste à allouer sur le heap la mémoire nécessaire pour l'objet en question. Les variables allouées sur le heap sont les chaînes de caractères, tableaux et objets tandis que les types valeurs (int, long, bool, …) et les structures sont alloués sur la pile.

int[] monTableauInt = new int[5];

Plus d'infos sur le mot clé ici.

null

null permet de mettre une référence à nulle. Par défaut, une référence vaut null.

// obj1 et obj2 valent null
object obj1;
object obj2 = null;

Plus d'infos sur le mot clé ici.

object

object est un alias de System.Object et représente la classe de base aussi bien pour les objets de type référence que valeur.

class Oiseau
{ }

static class Program
{
    static void Main()
    {
        // Casting d'un type valeur en object
        int monInt = 10;
        object monIntDansObject = monInt;

        // Casting d'un type référence en object
        Oiseau monOiseau = new Oiseau();
        object monOiseauDansObject = monOiseau;
    }
}

Plus d'infos sur le mot clé ici.

operator

operator est utilisé pour définir le comportement d'une classe par rapport à un opérateur arithmétique, de conversion ou encore logique.

class NombreEntier
{
    public int valeur;
    public NombreEntier(int param)
    {
        valeur = param;
    }

    // Opérateur arithmétique +
    public static NombreEntier operator +(NombreEntier a, NombreEntier b)
    {
        return new NombreEntier(a.valeur + b.valeur);
    }

    // Opérateurs logiques == et !=
    public static bool operator ==(NombreEntier a, NombreEntier b)
    {
        return a.valeur == b.valeur;
    }
    public static bool operator !=(NombreEntier a, NombreEntier b)
    {
        return a.valeur != b.valeur;
    }

    // Opérateur de conversion en int
    public static implicit operator int(NombreEntier a)
    {
        return a.valeur;
    }
}
static class Program
{
    static void Main()
    {
        NombreEntier monNombre1 = new NombreEntier(5);
        NombreEntier monNombre2 = new NombreEntier(6);

        // Opération +
        NombreEntier monNombre3 = monNombre1 + monNombre2;
        Console.WriteLine("monNombre3 vaut {0}", monNombre3.valeur);

        // Comparaison
        if (monNombre1 != monNombre3)
            Console.WriteLine("monNombre1 et monNombre3 sont différents");
        else
            Console.WriteLine("monNombre1 et monNombre3 sont égaux");

        // Conversion
        int monInt = (int)monNombre1;
        Console.WriteLine("monInt vaut {0}", monInt);

    }
}

Plus d'infos sur le mot clé ici.

out

out permet de passer un paramètre par référence sans qu'il ny ait besoin de l'initialiser.

static class Program
{
    static void Main()
    {
        int monInt;

        maFonction(out monInt);

        // monInt vaut 2007
        Console.WriteLine("monInt vaut {0}", monInt);
    }

    static void maFonction(out int param_int)
    {
        param_int = 2007;
    }
}

Plus d'infos sur le mot clé ici.

override

override permet de ré implémenter une méthode virtuelle ou abstraite d'une classe de base dans la classe dérivée.

Sélectionnez
public class classA
{
    public virtual void ExempleMethode()
    {
        Console.WriteLine("ExempleMethode de classA");
    }
}

public class classB : classA
{
    public override void ExempleMethode()
    {
        Console.WriteLine("Surcharge de ExempleMethode de classB");
    }
}

Plus d'infos sur le mot clé ici.

params

params est utilisé pour passer une nombre variable de paramètres à une fonction.

static void Main()
{
    MaFonctionVariable("Bonjour");
    MaFonctionVariable("Bonjour", "Au revoir");
}

static void MaFonctionVariable(params String[] MesParams)
{
    foreach (String courantString in MesParams)
        Console.WriteLine("Valeur du paramètre : {0}", courantString);
}

Plus d'infos sur le mot clé ici.

private

private est un qualificatif qui s'applique aux classes ainsi qu'aux champs et méthodes d'une classe. Dans ce cas, seules les méthodes de la même classe peuvent y avoir accès ce qui signifie qu'une classe dérivée ne pourra avoir accès à un champ ou une méthode qualifiée de private.

private class maClasse
{ }

Plus d'infos sur le mot clé ici.

protected

protected est un qualificatif qui s'applique aux classes ainsi qu'aux champs et méthodes d'une classe. Dans ce cas, seules les méthodes de la même classe ou des classes dérivées peuvent avoir accès à un champ ou une méthode qualifiée de protected.

protected class maClasse
{ }

Plus d'infos sur le mot clé ici.

public

public est un qualificatif qui s'applique aux classes ainsi qu'aux champs et méthodes d'une classe. Dans ce cas, n'importe quelle méthode de n'importe quelle classe peut avoir accès à un champ ou une méthode qualifiée de public.

public class maClasse
{ }

Plus d'infos sur le mot clé ici.

readonly

readonly permet de qualifier un champ en tant que constante qui pourra être initialisé au plus tard dans le constructeur de la classe.

class TestConstante
{
    readonly string monReadOnly;

    public TestConstante(string monParam)
    {
        monReadOnly = monParam;
    }
}

Plus d'infos sur le mot clé ici.

ref

ref permet de passer un paramètre par référence.

static class Program
{
    static void Main()
    {
        int monInt = 2006;

        // monInt vaut 2006
        Console.WriteLine("monInt avant l'appel de fonction vaut {0}", monInt);

        maFonction(ref monInt);

        // monInt vaut 2007
        Console.WriteLine("monInt après l'appel de fonction vaut {0}", monInt);
    }

    static void maFonction(ref int param_int)
    {
        param_int++;
    }
}

Plus d'infos sur le mot clé ici.

return

return permet de terminer l'exécution d'une méthode voir de retourner une valeur à la méthode appelante.

void ExampleReturn(int param)
{
    if (param == 0)
        // Met fin à l'exécution de la méthode
        return;
}

int ExampleReturnValeur(int param)
{
    // retourne le carré de la valeur passée en paramètre
    return param * param;
}

Plus d'infos sur le mot clé ici.

sbyte

sbyte est un alias de la structure System.SByte. Vous pourrez y stocker une valeur entière signée codée sur 8 bits (de -128 à 127).

sbyte monsByteAlias = -5;
SByte = monSByteStructure = monsByteAlias;

Plus d'infos sur le mot clé ici.

sealed

sealed permet d'empêcher une classe d'être dérivée.

sealed class ClassImpossibleAHeriter
{
    // ...
}

Plus d'infos sur le mot clé ici.

short

short est un alias de la structure System.Int16. Vous pourrez stocker dans ce type une valeur entière signée codée sur 16 bits (de -2^15 à 2^15 - 1).

short monShortAlias = 2007;
Int16 monInt16Structure = monShortAlias;

Plus d'infos sur le mot clé ici.

sizeof

sizeof permet de récupérer la taille en octet d'un type valeur.

static class Program
{
    static void Main()
    {
        Console.WriteLine("byte vaut {0} octets", sizeof(byte));
        Console.WriteLine("short vaut {0} octets", sizeof(short));
        Console.WriteLine("int vaut {0} octets", sizeof(int));
        Console.WriteLine("long vaut {0} octets", sizeof(long));
    }
}

Plus d'infos sur le mot clé ici.

stackalloc

stackalloc est utilisé dans un contexte unsafe et permet d'allouer un bloc de mémoire sur la pile.

int* psa = stackalloc int[100];

Plus d'infos sur le mot clé ici.

static

static permet de qualifier des champs comme des méthodes d'une classe qui existent sans instance de la classe. Ils sont donc communs à toutes les instances d'une classe.

Pour y accéder, on utilisera le nom de la classe.

static class Program
{
    static void Main()
    {
        // "Le compteur vaut 0"
        ExempleStatic.AfficheCompteur();

        ExempleStatic monObj1 = new ExempleStatic();

        // "Le compteur vaut 1"
        ExempleStatic.AfficheCompteur();

        ExempleStatic monObj2 = new ExempleStatic();

        // "Le compteur vaut 2"
        ExempleStatic.AfficheCompteur();
    }
}

/// <summary>
/// Classe qui compte le nombre d'instance
/// </summary>
class ExempleStatic
{
    static int Compteur = 0;

    public ExempleStatic()
    {
        Compteur++;
    }

    public static void AfficheCompteur()
    {
        Console.WriteLine("Le compteur vaut {0}", Compteur);
    }
}

Plus d'infos sur le mot clé ici.

string

string est un alias de System.String et permet de stocker une chaîne de caractères. Malgré le fait que ce soit un type référence, à chaque opération, un nouveau string est créé.

static class Program
{
    static void Main()
    {
        string monString = "Bonjour";
        System.String monStringAlias = monString;

        // "Le string vaut Bonjour"
        Console.WriteLine("Le string vaut {0}", monStringAlias);

        // "Le string vaut Bonjour tout le monde !!!"
        AfficheString(monStringAlias);

        // La référence n'a pas été modifiée
        // "Le string vaut Bonjour"
        Console.WriteLine("Le string vaut {0}", monStringAlias);
    }

    static void AfficheString(string param_String)
    {
        param_String = param_String + " tout le monde !!!";
        Console.WriteLine("Le string vaut {0}", param_String);
    }
}

La technique pour contourner ce problème est de passer la référence de la référence elle-même avec ref ou out :

static void Main()
{
    //...

    // La référence a enfin été modifiée
    // "Le string vaut Bonjour tout le monde !!!"
    Console.WriteLine("Le string vaut {0}", monStringAlias);
}

static void AfficheStringRef(ref string param_String)
{
    param_String = param_String + " tout le monde !!!";
}

Plus d'infos sur le mot clé ici.

struct

struct sert à déclarer une structure.

struct ExempleStructure
{
    int monInt;
    long monLong;
}

A noter qu'une structure est un type valeur étant donnée qu'elle dérive de System.ValueType.

Plus d'infos sur le mot clé ici.

switch

switch permet de faire un aiguillage vers une ou plusieurs instructions en fonction d'une variable de contrôle. La variable de contrôle peut être une valeur entière (byte, short, int, long), un caractère (char), une chaîne de caractères (string) ou encore une valeur d'énumération.

switch ((new Random()).Next(5))
{
    case 0: Console.WriteLine("La valeur vaut 0"); break;
    case 1: Console.WriteLine("La valeur vaut 1"); break;
    case 2: Console.WriteLine("La valeur vaut 2"); break;
    case 3: Console.WriteLine("La valeur vaut 3"); break;
    default: Console.WriteLine("La valeur n'est pas gérée"); break;
}

Plus d'infos sur le mot clé ici.

this

this est utilisé pour faire référence à l'objet en cours et ainsi accéder aux membres de l'objet en question.

class ExempleThis
{
    private String MonString;

    public ExempleThis()
    {
        MonString = "Hello";
    }

    private String ReturnWorld()
    {
        return "World";
    }

    public void DisplayHelloWorld()
    {
        Console.WriteLine(this.MonString + " " + this.ReturnWorld());
    }
}

Plus d'infos sur le mot clé ici.

throw

throw permet de lever une exception afin de signaler un problème.

void ExempleMethode(int param_Int)
{
    if (param_Int == 0)
        throw new Exception("Le paramètre ne peut être egal à 0 !!!");
}

Plus d'infos sur le mot clé ici.

true

true est un opérateur représentant la valeur vrai d'un bool (ou Boolean).

bool monBool = true;

Plus d'infos sur le mot clé ici.

try

try permet d'englober un bloc d'instructions qui est dit "protégé". C'est-à-dire que si une exception est levée dans ce bloc, elle sera traitée dans un des blocs catch qui suit.

try
{
    int i = 0;
    int j = 5;

    int ret = j / i;
}
catch (DivideByZeroException exc)
{
    Console.WriteLine("Voici le message de l'erreur : {0}", exc.Message);
}

Plus d'infos sur le mot clé ici.

typeof

typeof permet de récupérer un objet System.Type sur base d'un type.

Type monType = typeof(int);

Plus d'infos sur le mot clé ici.

uint

uint est un alias de la structure System.UInt32. Vous pourrez stocker dans ce type une valeur entière non signée codée sur 32 bits (de 0 à 4294967295).

uint monUIntAlias = 2007;
UInt32 monUInt32Structure = monUIntAlias;

Plus d'infos sur le mot clé ici.

ulong

ulong est un alias de la structure System.UInt64. Vous pourrez stocker dans ce type une valeur entière non signée codée sur 64 bits (de 0 à 18*10^18).

Sélectionnez
ulong monULongAlias = 1000000000;
UInt64 monUInt64Structure = monULongAlias;

Plus d'infos sur le mot clé ici.

unchecked

unchecked permet de désactiver le contrôle de dépassement de capacité sur des entiers ou lors de conversion.

unchecked
{
    int i = int.MaxValue + 1;
}

Sans le mot clé unchecked, l'instruction ne passerait même pas à la compilation car il s'agit d'une opération sur des constantes.

Plus d'infos sur le mot clé ici.

unsafe

unsafe permet de déclarer un bloc d'instructions pouvant utiliser les pointeurs.

unsafe
{
    int i = 2006, j;
    int* k;

    // k pointe sur i
    k = &i;

    // j contient 2007
    j = *k + 1;
}

A noter que pour utiliser les pointeurs dans un projet, il faut au préalable les activer. Pour cela allez dans les propriétés du projet, l'onglet "Build" et cochez la case "allow unsafe code".

Plus d'infos sur le mot clé ici.

ushort

ushort est un alias de la structure System.UInt16. Vous pourrez stocker dans ce type une valeur entière non signée codée sur 16 bits (de 0 à 65535).

ushort monUShortAlias = 2007;
UInt16 monUInt16Structure = monUShortAlias;

Plus d'infos sur le mot clé ici.

using

Il y a 3 utilisations pour le mot clé using.

Première utilisation: permettre l'utilisation de types dans un espace de noms :

using System;

namespace WindowsApplication3
{
    static class Program
    {
        static void Main()
        {
            System.Object obj1 = new System.Object();
            // ou
            Object obj2 = new Object();
        }
    }
}

Seconde utilisation : créer un alias en rapport avec un espace de noms :

using System;

using MonAlias = System.Object;

namespace WindowsApplication3
{
    static class Program
    {
        static void Main()
        {
            System.Object obj1 = new System.Object();
            // ou
            MonAlias obj2 = new MonAlias();
        }
    }
}

Troisième utilisation : définir la portée d'existence d'un objet. Pour être plus précis, la méthode Dispose de l'objet en question sera appelée automatiquement une fois que le bloc d'instructions sera terminé. Ce qui implique donc que l'objet utilisé dans un using devra implémenter l'interface IDisposable :

class Oiseau : IDisposable
{
    public void Dispose()
    { }
}

static class Program
{
    static void Main()
    {
        Oiseau monOiseau = new Oiseau();

        using (monOiseau)
        { }
    }
}

Plus d'infos sur le mot clé ici.

virtual

virtual est utilisé pour les méthodes, indexeurs, propriétés et évènements signifiant que ces derniers peuvent être ré implémentés dans une classe dérivée.

public class classA
{
    public virtual void ExempleMethode()
    {
        Console.WriteLine("ExempleMethode de classA");
    }
}

public class classB : classA
{
    public override void ExempleMethode()
    {
        Console.WriteLine("Surcharge de ExempleMethode de classB");
    }
}

Plus d'infos sur le mot clé ici.

volatile

volatile est utilisé sur les champs qui sont accédés par plusieurs threads différents. Cela permet que la dernière valeur du champ soit toujours présente.

Sélectionnez
public volatile string monStringVolatile = "Ma chaîne";

Plus d'infos sur le mot clé ici.

void

void est utilisé pour signaler qu'une méthode ne renvoie aucune valeur.

void ExempleMethode()
{ }

Plus d'infos sur le mot clé ici.

while

while représente une boucle. Tant que la condition renvoie vrai, le bloc d'instructions est exécuté. La condition est vérifiée avant d'exécuter le bloc d'instructions pour la première fois.

int i = 0;
while (i < 5)
{
    Console.WriteLine("i vaut {0}", i);

    i++;
}

Plus d'infos sur le mot clé ici.


CSharp tutoriel 2008

Bien commenter et documenter son code en C#

  • I. Introduction
  • II. Commenter le code
    • II-A. Que faut-il commenter dans le code ?
    • II-B. Comment le commenter ?
    • II-C. Conclusion sur les commentaires
  • III. Documenter le code
    • III-A. Documenter oui, mais comment ?
    • III-B. Les tags de documentations
      • III-B-1. Le tag « summary »
      • III-B-2. Le tag « param »
      • III-B-3. Le tag « returns »
      • III-B-4. Le tag « value »
      • III-B-5. Le tag « paramref »
      • III-B-6. Le tag « c »
      • III-B-7. Le tag « remarks »
      • III-B-8. Le tag « exception »
      • III-B-9. Le tag « example »
      • III-B-10. Le tag « code »
      • III-B-11. Le tag « see »
      • III-B-12. Le tag « seealso »
      • III-B-13. Le cas des listes
    • III-C. Comment générer la documentation ?
      • III-C-1. Génération de la documentation avec NDoc
      • IV. Remerciement
      • V. V. Liens utiles

I. Introduction

La lecture de ce tutoriel ne requiert aucune connaissance particulière si ce n'est les bases de la programmation en C#.

Tout au long de ce tutoriel je m'efforcerai de faire la distinction entre ce qui est admis par tous et les règles que j'applique moi même.

L'idée de ce tutoriel vient d'un sujet de discussion sur le forum.

Je tiens également à préciser que les exemples de code sont volontairement simplistes et parfois un développeur expérimenté ne trouvera pas les commentaires pertinents; toutefois le tutoriel vise un public débutant.

Tous les mots entre « » du tutoriel sont des tags de documentation. Tous les mots en gras sont des mots clefs du langage et tous les mots en italique sont des arguments des tags de documentation.

II. Commenter le code

Commenter le code … Tout le monde en a entendu parler, les développeurs/chefs de projets/professeurs le disent tout le temps : « Commente ton code STP ».

Mais voilà, que faut-il commenter ? Comment le commenter ?

C'est à ces deux principales questions que je vais essayer de répondre.

II-A. Que faut-il commenter dans le code ?

Assurément, il ne faut pas tout commenter dans un code. C'est d'ailleurs un des problèmes rencontrés assez régulièrement quand l'on travaille en équipe : soit le code n'est pas du tout commenté, soit il l'est trop.

Sachez que dans les deux cas le résultat est le même : le code est illisible.

Afin d'avoir un code facilement lisible par une tierce personne (ou par soi-même d'ailleurs), il faut donc le commenter de manière intelligente, c'est-à-dire ni trop ni pas assez et avec précision.

Exemple :

public void Main(string[] args)
{
        //Lancement
        Application.Run(new MainForm());
}

Ceci est un exemple parfait de ce qu'il ne faut pas faire. Le commentaire est imprécis et inutile. Dans un cas comme ça je préfère laisser au développeur le soin de chercher dans la documentation du framework à quoi sert Application.Run et comment l'on s'en sert. De plus cette ligne est une ligne que (normalement) tout développeur .NET a déjà vue au moins une fois dans sa vie. Le commentaire est donc superflu, car aucune information utile n'est apportée ici.

public void Switch(ref int a, ref int b)
{
    a *= b;
    b = a/b;
    a = a/b;
}

Ce code n'a rien d'extraordinaire mais il est déjà un peu plus complexe. Bien qu'il ne faille pas longtemps pour le comprendre, il peut être commenté afin de faciliter la lecture. Voilà comment je l'aurais commenté :

Les mots-clefs ref ne sont pas l'objet du tutoriel, si vous ne savez pas à quoi ils servent, la documentation du framework vous éclairera dessus.

public void Switch(ref int a, ref int b)
{
    //Effectue l'échange de a et b
    a *= b;
    b = a/b;
    a = a/b;

}

Ce commentaire permet de tout de suite comprendre ce que fait le code en dessous. Il est clair, précis et concis.

II-B. Comment le commenter ?

Dans ce paragraphe je ferais juste un petit rappel des techniques de commentaires et ensuite j'expliquerai comment commenter clairement le code.

Les différentes techniques de commentaire en C# sont les suivantes :

// Commentaire sur une ligne, idéal pour une description courte d'un code pas trop complexe
/*
  Commentaire sur plusieurs lignes pour une desciption longue.
  Cela permet d'aérer le commentaire afin de ne pas en faire un gros pavé illisible.
  Je pourrais m'étendre encore longtemps comme ça mais vous avez compris je pense ;-)
*/

Ce sont là deux grandes méthodes de commentaire dans un code C#.

Revenons plutôt au code précédent afin d'en analyser le commentaire :

public void Switch(ref int a, ref int b)
{
    //Effectue l'échange de a et b
    a *= b;
    b = a/b;
    a = a/b;

}

À ce stade, deux choix sont possibles :

  • soit le code commenté n'est qu'une partie annexe de l'algorithme et ce type de commentaire suffit, car il n'est pas indispensable de comprendre parfaitement le code en dessous pour comprendre le programme ;
  • soit le code commenté est un « pilier » du programme auquel cas il faut plus le détailler.

Partons de l'hypothèse que ce code est un des piliers de votre nouvelle bibliothèque de maths et regardons comment nous pourrions le commenter un peu plus :

public void Switch(ref int a, ref int b)
{
    //Effectue l'échange de a et b
    a *= b;

    //b vaut maintenant a (b = (ab)/b d'ou b = a)
    b = a/b;
    //a vaut maintenant b (a = (ab)/a (car b = a) d'ou a = b)
    a = a/b;

}

Comme vous le voyez, les différentes étapes (stades) de l'algorithme sont commentés et justifiés afin d'en faciliter la compréhension.

L'exemple étant fictif et simpliste, le commentaire perd de sa pertinence et de sa justification mais bon il est là à titre d'exemple.

II-C. Conclusion sur les commentaires

Il n'existe pas de règles de commentaires autre que le bon sens. Ces commentaires reflètent la manière dont je travaille et peuvent ne pas vous convenir.

C'est d'ailleurs un des problèmes du travail à plusieurs : un développeur peut trouver qu'un commentaire est justifié alors qu'un autre ne le pense pas.

III. Documenter le code

Après avoir vu comment commenter le code afin de le partager avec ses collègues/amis/équipiers, nous allons voir comment le documenter afin qu'une personne externe (ou interne) au projet puisse utiliser ce que l'on a développé.

La documentation est un point essentiel de toute librairie destinée à être utilisée par d'autres développeurs. Imaginez que l'équipe qui a développé .NET n'ait pas du tout documenté le code. Il serait impossible de consulter la documentation de telle ou telle classe afin de savoir comment il faut utiliser une méthode.

Impensable de nos jours.

C'est pour cela que les tags de documentation sont là. Ils vous permettent de documenter le code à l'intérieur de celui-ci afin que la documentation soit partie intégrante du projet.

Ainsi chaque développeur a la responsabilité de la documentation de son code. Et d'ailleurs qui d'autre que le développeur qui a écrit cette fonction peut mieux la documenter ?

Dans cette partie nous verrons comment documenter une classe, une fonction, une propriété et ensuite nous verrons un outil qui permet de générer la documentation au format HTML et chm (HTML compilé) à partir du fichier .xml généré par le compilateur C#.

III-A. Documenter oui, mais comment ?

Afin de documenter le code il existe un format de commentaire particulier qui indique au compilateur de traiter les commentaires comme de la documentation.

En fait il en existe plusieurs, mais seul le format le plus courant sera traité ici. Pour plus d'informations voir la documentation du framework.

Ce format est tout simple :

///

Il s'agit de trois slash (/) à la suite. Dans Visual Studio et SharpDevelop, le fait de mettre trois slash à la suite dans un endroit valide fait apparaître un menu avec les tags de documentation. C# builder 1.0 n'a hélas pas le même comportement.

Bien, vous savez donc maintenant informer le compilateur que ce qui va suivre doit être de la documentation.

III-B. Les tags de documentations

Le langage C# définit une vingtaine de tags de documentation afin de gérer toutes les possibilitées et ainsi d'avoir une documentation claire et standard.

III-B-1. Le tag « summary »

Le tag « summary » sert à donner la description complète de l'élément que l'on souhaite documenter. Il peut être utilisé sur une classe, une fonction, propriété ou une variable.

Ce tag est un des tags de base de la documentation.

/// <summary>
///  Description complète de la classe. Généralement on donne la fonction de cette classe ainsi que ces particularités
/// </summary>
public class MaClasse
{
    /// <summary>
    /// Description de la variable et de son rôle dans le programme/classe.
    /// </summary>
    public static int maVariable;

    /// <summary>
    /// Description complète de la fonction. Généralement on donne le but de la fonction.
    /// </summary>
    public void UneMethode()
    {
     ...
    }

    /// <summary>
    /// Description complète de la propriété.
    /// Attention il n'est pas nécessaire de préciser si elle est en lecture seule ou non,
    /// le fait d'avoir seulement le get permet au générateur de documentation de marqué la propriété comme étant en lecture seule.
    /// </summary>
    public int UnePropriete
    {
        get
        {
            ...
        }
    }
}

Comme vous le constatez la balise « summary » n'est pas difficile à utiliser. Cette balise devrait être présente sur toutes les classes, méthodes, propriétés, variables marquées « public » d'un programme.

III-B-2. Le tag « param »

Le tag « param » permet de documenter les paramètres d'une fonction ou d'une propriété. Il prend en complément le nom de la variable. Le type de la variable est automatiquement déterminé par le générateur de documentation.

/// <summary>
///  Description complète de la classe. Généralement on donne la fonction de cette classe ainsi que ces particularités
/// </summary>
public class MaClasse
{
    /// <summary>
    /// Une méthode qui ne fait rien ;-)
    /// </summary>
    /// <param name=a>parametre qui ne sert à rien du tout.</param>
    public void UneMethode(int a)
    {

    }
}

Cette balise est pratiquement indissociable de la balise « summary » pour les fonctions.

III-B-3. Le tag « returns »

Le tag « returns » permet de documenter la valeur de retour d'une fonction seulement. Son utilisation est très simple.

/// <summary>
///  Description complète de la classe. Généralement on donne la fonction de cette classe ainsi que ces particularités
/// </summary>
public class MaClasse
{
    /// <summary>
    /// Une méthode qui ne fait rien ;-)
    /// </summary>
    /// <param name=a>parametre qui ne sert à rien du tout.</param>
    /// <returns>Valeur passée en argument non modifiée car la fonction ne fait rien .</returns>
    public int UneMethode(int a)
    {
        return a;
    }
}

III-B-4. Le tag « value »

Le tag « value » permet de décrire la valeur de retour ou d'entrée d'une propriété. Elle joue le même rôle que la balise « param » pour une fonction.

/// <summary>
///  Description complète de la classe. Généralement on donne la fonction de cette classe ainsi que ces particularités
/// </summary>
public class MaClasse
{
    /// <summary>
    ///    Une propriétée.
    ///    </summary>
    ///    <value>Renvois toujours 10</value>
    public int UnePropriete
    {
        get
        {
            return 10;
        }
    }
}

III-B-5. Le tag « paramref »

Le tag « paramref » permet d'indiquer que le mot dans le commentaire est un paramètre de la fonction. Cela permet au générateur de documentation de faire la distinction lors de la génération.

/// <summary>
///  Description complète de la classe. Généralement on donne la fonction de cette classe ainsi que ces particularités
/// </summary>
public class MaClasse
{
    /// <summary>
    /// Une méthode qui ne fait rien ;-) avec <paramref name="a" />
    /// </summary>
    /// <param name=a>parametre qui ne sert à rien du tout.</param>
    /// <returns>Valeur passée en argument non modifiée car la fonction ne fait rien .</returns>
    public int UneMethode(int a)
    {
        return a;
    }
}

III-B-6. Le tag « c »

Le tag « c » permet d'indiquer que le mot est un mot faisant partie du code.

/// <summary>
///  Description complète de la classe. Généralement on donne la fonction de cette classe ainsi que ces particularités
///  Possede une méthode <c>UneMethode</c> qui ne sert à rien.
/// </summary>
public class MaClasse
{
}

III-B-7. Le tag « remarks »

Le tag « remarks » permet de mettre des remarques sur une classe.

/// <summary>
///  Description complète de la classe. Généralement on donne la fonction de cette classe ainsi que ces particularités
///  Possede une méthode <c>UneMethode</c> qui ne sert à rien.
/// </summary>
/// <remarks>
///        Une remarque fort interessante peut être placé ici.
/// </remarks>
public class MaClasse
{
}

III-B-8. Le tag « exception »

Le tag « exception » permet d'informer sur le(s) type(s) d'exception(s) que la fonction peut lever.

Vous devez donner dans le tag, la condition pour que l'exception soit déclenchée. La propriété cref du tag permet de spécifier le type d'exception que vous documentez.

/// <summary>
///  Description complète de la classe. Généralement on donne la fonction de cette classe ainsi que ces particularités
///  Possede une méthode <c>UneMethode</c> qui ne sert à rien.
/// </summary>
/// <remarks>
///        Une remarque fort interessante peut être placé ici.
/// </remarks>
public class MaClasse
{
    /// <summary>
    /// Une méthode qui ne fait rien ;-) avec <paramref name="a" />
    /// </summary>
    /// <param name=a>parametre qui ne sert à rien du tout.</param>
    /// <returns>Valeur passée en argument non modifiée car la fonction ne fait rien .</returns>
    /// <exception cref="Type d'exception">Exception déclenchée quand le programme plante ;-) </exception>
    public int UneMethode(int a)
    {
        return a;
    }
}

III-B-9. Le tag « example »

Le tag « example » permet de spécifier le texte accompagnant un exemple d'utilisation d'une méthode ou d'une classe.

/// <summary>
///  Description complète de la classe. Généralement on donne la fonction de cette classe ainsi que ces particularités
///  Possede une méthode <c>UneMethode</c> qui ne sert à rien.
/// </summary>
/// <remarks>
///        Une remarque fort interessante peut être placé ici.
/// </remarks>
public class MaClasse
{
    /// <summary>
    /// Une méthode qui ne fait rien ;-) avec <paramref name="a" />
    /// </summary>
    /// <param name=a>parametre qui ne sert à rien du tout.</param>
    /// <returns>Valeur passée en argument non modifiée car la fonction ne fait rien .</returns>
    /// <exception cref="Type d'exception">Exception déclenchée quand le programme plante ;-) </exception>
    /// <example>Voila un exemple d'utilisation de la méthode </example>
    public int UneMethode(int a)
    {
        return a;
    }
}

III-B-10. Le tag « code »

Le tag « code » permet de marquer le texte qui suit comme étant du code.

/// <summary>
///  Description complète de la classe. Généralement on donne la fonction de cette classe ainsi que ces particularités
///  Possede une méthode <c>UneMethode</c> qui ne sert à rien.
/// </summary>
/// <remarks>
///        Une remarque fort interessante peut être placé ici.
/// </remarks>
public class MaClasse
{
    /// <summary>
    /// Une méthode qui ne fait rien ;-) avec <paramref name="a" />
    /// </summary>
    /// <param name=a>parametre qui ne sert à rien du tout.</param>
    /// <returns>Valeur passée en argument non modifiée car la fonction ne fait rien .</returns>
    /// <exception cref="Type d'exception">Exception déclenchée quand le programme plante ;-) </exception>
    /// <example>Voila un exemple d'utilisation de la méthode
    /// <code>this.UneMethode(10);</code>
    /// </example>
    public int UneMethode(int a)
    {
        return a;
    }
}

III-B-11. Le tag « see »

Le tag « see » permet de créer un lien vers un élément documenté grâce au nom de cet élément.

Il doit être placé à l'intérieur d'un tag « summary » ou « remarks » :

/// <summary>
///  Description complète de la classe. Généralement on donne la fonction de cette classe ainsi que ces particularités
///  Possede une méthode <c>UneMethode</c> qui ne sert à rien.
/// </summary>
/// <remarks>
///        Une remarque fort interessante peut être placé ici.
///        <see cref="Main">Point d'entrée du programme</see>
/// </remarks>
public class MaClasse
{

}

Si vous voulez faire pointer le lien vers un élément du framework il faut renseigner cref comme habituellement mais il faut dire au générateur de documentation que vous autorisez les liens vers la MSDN.

III-B-12. Le tag « seealso »

Le tag « seealso » permet de créer un lien en bas de la page de documentation. Il est utilisé pour renvoyer le lecteur à un complément d'information.

/// <summary>
///  Description complète de la classe. Généralement on donne la fonction de cette classe ainsi que ces particularités
///  Possede une méthode <c>UneMethode</c> qui ne sert à rien.
/// </summary>
/// <remarks>
///        Une remarque fort interessante peut être placé ici.
///        <see cref="Main">Point d'entrée du programme</see>
/// </remarks>
/// <seealso cref="System.Object">System.Object</seealso>
public class MaClasse
{

}

III-B-13. Le cas des listes

L'utilisation des listes est un peu plus compliqué que le reste.

En effet il faut composer avec 4 tags différents. La liste doit être placée dans un tag « remarks ».

/// <summary>
///  Description complète de la classe. Généralement on donne la fonction de cette classe ainsi que ces particularités
///  Possede une méthode <c>UneMethode</c> qui ne sert à rien.
/// </summary>
/// <remarks>
///        Une remarque fort interessante peut être placé ici.
///        <see cref="Main">Point d'entrée du programme</see>
///        <list type="bullet" ou "number" ou "table">
///            <listheader>
///                <term>Le titre de la partie 1 du tableau (seulement en mode "table" </term>
///                <description>Le titre de la partie 2 du tableau (seulement en mode "table" </description>
///            </listheader>
///            <item>
///                <term>le terme à expliquer </item>
///                <description> La description du terme </description>
///            </item>
///        </list>
/// </remarks>
public class MaClasse
{

}

Il faut autant de balise item que vous voulez de ligne.

Bien que l'utilisation de la liste soit un peu plus compliquée, ce n'est rien de bien méchant.

III-C. Comment générer la documentation ?

Afin de générer la documentation il vous faut générer le fichier XML grâce au compilateur C# et à l'option /doc.

mcs /out:unProgramme.exe /t:exe /doc ./*cs

L'autre façon de générer la documentation est d'utiliser votre EDI préféré afin de lui demander de générer le fichier XML.

Sous SharpDevelop : Onglets Projets -> Click droit sur le projet -> Options du projet -> Configuration -> Debug ou Release -> Génération de code -> Générer la documentation Xml.

Image non disponible

Génération du fichier Xml de documentation sous SharpDevelop

Sous Visual Studio 2005 Beta 2: Solution Explorer -> Build -> Xml Documentation File :

Image non disponible

Génération du fichier XML de documentation sous Visual Studio 2005

Une fois que vous avez le fichier XML vous pouvez utiliser le programme NDoc qui vous permet de générer la documentation sous différents styles comme la MSDN, la javadoc ou encore LaTex.

III-C-1. Génération de la documentation avec NDoc ▲

La génération de la documentation avec NDoc à partir du fichier XMl est très simple.

Il vous suffit de lancer l'interface graphique de l'outil (NdocGui.exe) et de renseigner les assemblies dont vous souhaitez générer la documentation.

Pour ce faire il suffit de cliquer sur Add puis de sélectionner l'assembly. Si le fichier Xml correspondant est dans le même répertoire que l'assembly, NDoc le sélectionne automatiquement. Si ce n'est pas le cas il vous suffit de l'ajouter vous-même.

Image non disponible

Sélection de l'assembly et de son fichier XML

Il ne vous reste plus qu'à lancer la génération par le biais du bouton dans la bar d'outils de Ndoc.

Le but n'étant pas de faire un tutoriel complet sur la génération de documentation avec NDoc, je ne vous parlerai donc pas des divers options possibles de NDoc.

IV. Remerciement

Je tiens à remercier pharaonix et nightfall pour leur relecture.

V. V. Liens utiles

NDoc
SharpDevelop (intègre NDoc)
téléchargement de « Visual C# express edition »


CSharp tutoriel 2008

Apprentissage du langage C# Vu dans ... http://tahe.developpez.com/dotnet/csharp

  • Introduction
    • II. Introduction
  • Installation de Visual C# 2008
    • III. Installation de Visual C# 2008
  • Les bases du langage C#
    • IV. Les bases du langage C#
      • IV-A. Introduction
      • IV-B. Les données de C#
      • IV-C. Les types de données prédéfinis
        • IV-C-1. Notation des données littérales
        • IV-C-2. Déclaration des données
          • IV-C-2-a. Rôle des déclarations
          • IV-C-2-b. Déclaration des constantes
          • IV-C-2-c. Déclaration des variables
        • IV-C-3. Les conversions entre nombres et chaînes de caractères
        • IV-C-4. Les tableaux de données
      • IV-D. Les instructions élémentaires de C#
        • IV-D-1. Ecriture sur écran
        • IV-D-2. Lecture de données tapées au clavier
        • IV-D-3. Exemple d'entrées-sorties
        • IV-D-4. Redirection des E/S
        • IV-D-5. Affectation de la valeur d'une expression à une variable
          • IV-D-5-a. Interprétation de l'opération d'affectation
          • IV-D-5-b. Expression arithmétique
          • IV-D-5-c. Priorités dans l'évaluation des expressions arithmétiques
          • IV-D-5-d. Expressions relationnelles
          • IV-D-5-e. Expressions booléennes
          • IV-D-5-f. Traitement de bits
          • IV-D-5-g. Combinaison d'opérateurs
          • IV-D-5-h. Opérateurs d'incrémentation et de décrémentation
          • IV-D-5-i. L'opérateur ternaire ?
          • IV-D-5-j. Priorité générale des opérateurs
          • IV-D-5-k. Les changements de type
      • IV-E. Les instructions de contrôle du déroulement du programme
        • IV-E-1. Arrêt
        • IV-E-2. Structure de choix simple
        • IV-E-3. Structure de cas
        • IV-E-4. Structures de répétition
          • IV-E-4-a. Nombre de répétitions connu
          • IV-E-4-b. Nombre de répétitions inconnu
          • IV-E-4-c. Instructions de gestion de boucle
      • IV-F. La gestion des exceptions
      • IV-G. Application exemple - version 1
      • IV-H. Arguments du programme principal
      • IV-I. Les énumérations
      • IV-J. Passage de paramètres à une fonction
        • IV-J-1. Passage par valeur
        • IV-J-2. Passage par référence
        • IV-J-3. Passage par référence avec le mot clé out
  • Classes, Stuctures, Interfaces
    • V. Classes, Stuctures, Interfaces
      • V-A. L' objet par l'exemple
        • V-A-1. Généralités
        • V-A-2. Création du projet C#
        • V-A-3. Définition de la classe Personne
        • V-A-4. La méthode Initialise
        • V-A-5. L'opérateur new
        • V-A-6. Le mot clé this
        • V-A-7. Un programme de test
        • V-A-8. Une autre méthode Initialise
        • V-A-9. Constructeurs de la classe Personne
        • V-A-10. Les références d'objets
        • V-A-11. Passage de paramètres de type référence d'objet
        • V-A-12. Les objets temporaires
        • V-A-13. Méthodes de lecture et d'écriture des attributs privés
        • V-A-14. Les propriétés
        • V-A-15. Les méthodes et attributs de classe
        • V-A-16. Un tableau de personnes
      • V-B. L'héritage par l'exemple
        • V-B-1. Généralités
        • V-B-2. Construction d'un objet Enseignant
        • V-B-3. Redéfinition d'une méthode ou d'une propriété
        • V-B-4. Le polymorphisme
        • V-B-5. Redéfinition et polymorphisme
      • V-C. Redéfinir la signification d'un opérateur pour une classe
        • V-C-1. Introduction
        • V-C-2. Un exemple
      • V-D. Définir un indexeur pour une classe
      • V-E. Les structures
      • V-F. Les interfaces
      • V-G. Les classes abstraites
      • V-H. Les classes, interfaces, méthodes génériques
      • V-I. Les espaces de noms
      • V-J. Application exemple - version 2
  • Classes .NET d'usage courant
    • VI. Classes .NET d'usage courant
      • VI-A. Chercher de l'aide sur les classes .NET
        • VI-A-1. Help/Contents
        • VI-A-2. Help/Index/Search
      • VI-B. Les chaînes de caractères
        • VI-B-1. La classe System.String
        • VI-B-2. La classe System.Text.StringBuilder
      • VI-C. Les tableaux
      • VI-D. Les collections génériques
        • VI-D-1. La classe générique List<T>
        • VI-D-2. La classe Dictionary<TKey,TValue>
      • VI-E. Les fichiers texte
        • VI-E-1. La classe StreamReader
        • VI-E-2. La classe StreamWriter
      • VI-F. Les fichiers binaires
      • VI-G. Les expressions régulières
        • VI-G-1. Vérifier qu'une chaîne correspond à un modèle donné
        • VI-G-2. Trouver toutes les occurrences d'un modèle dans une chaîne
        • VI-G-3. Récupérer des parties d'un modèle
        • VI-G-4. Un programme d'apprentissage
        • VI-G-5. La méthode Split
      • VI-H. Application exemple - version 3
  • Architectures 3 couches
    • VII. Architectures 3 couches
      • VII-A. Introduction
      • VII-B. Les interfaces de l'application [Impots]
      • VII-C. Application exemple - version 4
        • VII-C-1. Le projet Visual Studio
        • VII-C-2. Les entités de l'application
        • VII-C-3. La couche [dao]
        • VII-C-4. La couche [metier]
        • VII-C-5. La couche [ui]
        • VII-C-6. Conclusion
      • VII-D. Application exemple - version 5
        • VII-D-1. NUnit
        • VII-D-2. La solution Visual Studio
        • VII-D-3. La couche [dao]
        • VII-D-4. La couche [metier]
        • VII-D-5. La couche [ui]
        • VII-D-6. La couche [Spring]
  • Interfaces graphiques avec C# et VS.NET
    • VIII. Interfaces graphiques avec C# et VS.NET
      • VIII-A. Les bases des interfaces graphiques
        • VIII-A-1. Un premier projet
        • VIII-A-2. Un second projet
          • VIII-A-2-a. Le formulaire
          • VIII-A-2-b. Le code lié à la gestion des événements
          • VIII-A-2-c. Conclusion
      • VIII-B. Les composants de base
        • VIII-B-1. Formulaire Form
        • VIII-B-2. Etiquettes Label et boîtes de saisie TextBox
        • VIII-B-3. Listes déroulantes ComboBox
        • VIII-B-4. Composant ListBox
        • VIII-B-5. Cases à cocher CheckBox, boutons radio ButtonRadio
        • VIII-B-6. Variateurs ScrollBar
      • VIII-C. Événements souris
      • VIII-D. Créer une fenêtre avec menu
      • VIII-E. Composants non visuels
        • VIII-E-1. Boîtes de dialogue OpenFileDialog et SaveFileDialog
        • VIII-E-2. Boîtes de dialogue FontColor et ColorDialog
        • VIII-E-3. Timer
      • VIII-F. Application exemple - version 6
        • VIII-F-1. La solution Visual Studio
        • VIII-F-2. La classe [Program.cs]
        • VIII-F-3. Le formulaire [Form1]
      • VIII-G. Le formulaire [Form2]
        • VIII-G-1. Conclusion
  • Evénements utilisateur
    • IX. Evénements utilisateur
      • IX-A. Objets delegate prédéfinis
      • IX-B. Définir des objets delegate
      • IX-C. Delegates ou interfaces ?
      • IX-D. Gestion d'événements
  • Accès aux bases de données
    • X. Accès aux bases de données
      • X-A. Connecteur ADO.NET
      • X-B. Les deux modes d'exploitation d'une source de données
      • X-C. Les concepts de base de l'exploitation d'une base de données
        • X-C-1. La base de données exemple
        • X-C-2. Les quatre commandes de base du langage SQL
        • X-C-3. Les interfaces de base d'ADO.NET pour le mode connecté
        • X-C-4. La gestion des erreurs
        • X-C-5. Configuration du projet exemple
        • X-C-6. Le programme exemple
        • X-C-7. Exécution d'une requête SELECT
        • X-C-8. Exécution d'un ordre de mise à jour : INSERT, UPDATE, DELETE
      • X-D. Autres connecteurs ADO.NET
        • X-D-1. Connecteur SQL Server 2005
        • X-D-2. Connecteur MySQL5
        • X-D-3. Connecteur ODBC
        • X-D-4. Connecteur OLE DB
        • X-D-5. Connecteur générique
        • X-D-6. Quel connecteur choisir ?
      • X-E. Requêtes paramétrées
      • X-F. Transactions
        • X-F-1. Généralités
        • X-F-2. L'API de gestion des transactions
        • X-F-3. Le programme exemple
      • X-G. La méthode ExecuteScalar
      • X-H. Application exemple - version 7
        • X-H-1. La base de données
        • X-H-2. La solution Visual Studio
        • X-H-3. La couche [dao]
        • X-H-4. La couche [metier]
        • X-H-5. La couche [ui]
        • X-H-6. Changer la base de données
      • X-I. Pour aller plus loin …
  • Les threads d'exécution
    • XI. Les threads d'exécution
      • XI-A. La classe Thread
      • XI-B. Création de threads d'exécution
      • XI-C. Intérêt des threads
      • XI-D. Echange d'informations entre threads
      • XI-E. Accès concurrents à des ressources partagées
        • XI-E-1. Accès concurrents non synchronisés
        • XI-E-2. La clause lock
        • XI-E-3. La classe Mutex
        • XI-E-4. La classe AutoResetEvent
        • XI-E-5. La classe Interlocked
      • XI-F. Accès concurrents à des ressources partagées multiples
        • XI-F-1. Un exemple
        • XI-F-2. La classe Monitor
      • XI-G. Les pools de threads
      • XI-H. La classe BackgroundWorker
        • XI-H-1. Exemple 1
        • XI-H-2. Exemple 2
      • XI-I. Données locales à un thread
        • XI-I-1. Le principe
        • XI-I-2. Application du principe
        • XI-I-3. Conclusion
      • XI-J. Pour approfondir…
  • Programmation Internet
    • XII. Programmation Internet
      • XII-A. Généralités
        • XII-A-1. Les protocoles de l'Internet
        • XII-A-2. Le modèle OSI
        • XII-A-3. Le modèle TCP/IP
        • XII-A-4. Fonctionnement des protocoles de l'Internet
        • XII-A-5. Les problèmes d'adressage dans l'Internet
          • XII-A-5-a. Les classes d'adresses IP
          • XII-A-5-b. Les protocoles de conversion Adresse Internet <--> Adresse physique
        • XII-A-6. La couche réseau dite couche IP de l'internet
          • XII-A-6-a. Le routage
          • XII-A-6-b. Messages d'erreur et de contrôle
        • XII-A-7. La couche transport : les protocoles UDP et TCP
          • XII-A-7-a. Le protocole UDP : User Datagram Protocol
          • XII-A-7-b. Le protocole TCP : Transfer Control Protocol
        • XII-A-8. La couche Applications
        • XII-A-9. Conclusion
      • XII-B. Les classes .NET de la gestion des adresses IP
      • XII-C. Les bases de la programmation internet
        • XII-C-1. Généralités
        • XII-C-2. Les caractéristiques du protocole TCP
        • XII-C-3. La relation client-serveur
        • XII-C-4. Architecture d'un client
        • XII-C-5. Architecture d'un serveur
      • XII-D. Découvrir les protocoles de communication de l'internet
        • XII-D-1. Introduction
        • XII-D-2. Le protocole HTTP (HyperText Transfer Protocol)
        • XII-D-3. Le protocole SMTP (Simple Mail Transfer Protocol)
        • XII-D-4. Le protocole POP (Post Office Protocol)
        • XII-D-5. Le protocole FTP (File Transfer Protocol)
      • XII-E. Les classes .NET de la programmation internet
        • XII-E-1. Choisir la classe adaptée
        • XII-E-2. La classe TcpClient
        • XII-E-3. La classe TcpListener
      • XII-F. Exemples de clients / serveurs TCP
        • XII-F-1. Un serveur d'écho
        • XII-F-2. Un client pour le serveur d'écho
        • XII-F-3. Un client TCP générique
        • XII-F-4. Un serveur Tcp générique
        • XII-F-5. Un client Web
        • XII-F-6. Un client Web gérant les redirections
      • XII-G. Les classes .NET spécialisées dans un protocole particulier de l'internet
        • XII-G-1. La classeWebClient
        • XII-G-2. Les classes WebRequest / WebResponse
        • XII-G-3. Application : un client proxy d'un serveur web de traduction
          • XII-G-3-a. L'application
          • XII-G-3-b. L'architecture de l'application
          • XII-G-3-c. Le projet Visual studio
          • XII-G-3-d. Le projet [dao]
          • XII-G-3-e. Le client web [ServiceTraduction]
          • XII-G-3-f. L'interface graphique de l'application
          • XII-G-3-g. Conclusion
        • XII-G-4. Un client SMTP (Simple Mail Tranport Protocol) avec la classe SmtpClient
      • XII-H. Un client Tcp générique asynchrone
        • XII-H-1. Présentation
        • XII-H-2. L'interface graphique du client Tcp asynchrone
        • XII-H-3. Connexion asynchrone au serveur
        • XII-H-4. Déconnexion du serveur
        • XII-H-5. Envoi asynchrone de données au serveur
        • XII-H-6. Affichage des événements et du dialogue client / serveur
        • XII-H-7. Conclusion
      • XII-I. Application exemple, version 8 : Serveur de calcul d'impôts
        • XII-I-1. L'architecture de la nouvelle version
        • XII-I-2. Le serveur de calcul d'impôt
          • XII-I-2-a. Le projet Visual Studio
          • XII-I-2-b. Configuration de l'application
          • XII-I-2-c. Fonctionnement du serveur
        • XII-I-3. Le code du serveur Tcp de calcul d'impôt
        • XII-I-4. Le client graphique du serveur Tcp de calcul d'impôt
          • XII-I-4-a. Le projet Visual Studio
          • XII-I-4-b. La couche [metier]
          • XII-I-4-c. La couche [ui]
        • XII-I-5. Conclusion
  • Services Web
    • XIII. Services Web
      • XIII-A. Introduction
      • XIII-B. Un premier service Web avec Visual Web Developer
        • XIII-B-1. La partie serveur
        • XIII-B-2. La partie client
      • XIII-C. Un service Web d'opérations arithmétiques
        • XIII-C-1. La partie serveur
        • XIII-C-2. La partie client
      • XIII-D. Un service web de calcul d'impôt
        • XIII-D-1. La partie serveur
        • XIII-D-2. Un client graphique windows pour le service web distant
          • XIII-D-2-a. La nouvelle couche [metier]
          • XIII-D-2-b. La nouvelle couche [ui]
      • XIII-E. Un client web pour le service web de calcul d'impôt
      • XIII-F. Un client console Java pour le service web de calcul d'impôt
  • A suivre…
    • XIV. A suivre…
  • Annexes
    • XV. Annexes
      • XV-A. Le SGBD SQL Server Express 2005
        • XV-A-1. Installation
        • XV-A-2. Lancer / Arrêter SQL Server
        • XV-A-3. Création d'un utilisateur jpa et d'une base de données jpa
        • XV-A-4. Création de la table [ARTICLES] de la base de données jpa
        • XV-A-5. Le connecteur ADO.NET de SQL Server Express
      • XV-B. Le SGBD MySQL5
        • XV-B-1. Installation
        • XV-B-2. Lancer / Arrêter MySQL5
        • XV-B-3. Clients d'administration MySQL
        • XV-B-4. Création d'un utilisateur jpa et d'une base de données jpa
        • XV-B-5. Installation du connecteur ADO.NET de MySQL5
        • XV-B-6. Installation du pilote ODBC de MySQL5

Introduction au langage C# par l'exemple :

  • Chap 1 : Les bases du langage C#
  • Chap 2 : Classes, Structures, Interfaces
  • Chap 3 : Classes .NET d'usage courant
  • Chap 4 : Architectures 3 couches, tests unitaires NUnit, framework Spring.net
  • Chap 5 : Interfaces graphiques
  • Chap 6 : Evénements utilisateur
  • Chap 7 : Accès aux base de données
  • Chap 8 : Threads d'exécution
  • Chap 9 : Programmation Internet
  • Chap 10 : Services Web

Le cours est accompagné de plus d'une centaine de programmes illustrant les différents aspects de C#. Une application exemple est déclinée en 10 versions tout au long du document. Elle utilise une architecture multicouches assemblées par Spring. Chaque version change une ou plusieurs couches de la version précédente. On passe ainsi au fil des chapitres, de l'application console monocouche de la version 1 à la version multicouches client / serveur de la version 10 avec un client à 2 couches : interface graphique / client de service web et un serveur à 3 couches : service web / couche métier / couche d'accès aux données / SGBD.

  • Téléchargement du fichier PDF du cours (453 pages - 20 Mo) : ftp (plus rapide) http (plus lent)
  • Téléchargement des projets Visual Studio Express des exemples (19 Mo) : ftp (plus rapide) http (plus lent)

A lire également :

  • Introduction à Entity Framework 5 Code First par l'exemple
  • Introduction à ASP.NET MVC par l'exemple
  • Etude de cas avec ASP.NET 2.0, C#, Spring.NET et NHibernate

Les autres ressources du site [http://tahe.developpez.com]


CSharp tutoriel 2008

Apprentissage du langage C# Vu dans ... http://tahe.developpez.com/dotnet/csharp

I. Remerciements

Je remercie les personnes suivantes :

  • les bénévoles de [developpez.com] qui permettent aux auteurs d'articles de délivrer des documents d'excellente qualité à leurs lecteurs ;
  • Joachim Tahé qui a mis le document initial au format [developpez.com]. Indispensable travail mais long travail.
Vu dans ... http://tahe.developpez.com/dotnet/csharp


CSharp tutoriel 2008

Apprentissage du langage C#

  • II. Introduction

II. Introduction

C# est un langage récent. Il a été disponible en versions beta successives depuis l'année 2000 avant d'être officiellement disponible en février 2002 en même temps que la plate-forme .NET 1.0 de Microsoft à laquelle il est lié. C# ne peut fonctionner qu'avec cet environnement d'exécution. Celui-ci rend disponible aux programmes qui s'exécutent en son sein un ensemble très important de classes. En première approximation, on peut dire que la plate-forme .NET est un environnement d'exécution analogue à une machine virtuelle Java. On peut noter cependant deux différences :

  • Java s'exécute sur différents OS (windows, unix, macintosh) depuis ses débuts. En 2002, la plate-forme .NET ne s'exécutait que sur les machines Windows. Depuis quelques années le projet Mono [http://www.monoproject.com] permet d'utiliser la plate-forme .NET sur des OS tels que Unix et Linux. La version actuelle de Mono (février 2008) supporte .NET 1.1 et des éléments de .NET 2.0.
  • la plate-forme .NET permet l'exécution de programmes écrits en différents langages. Il suffit que le compilateur de ceux-ci sache produire du code IL (Intermediate Language), code exécuté par la machine virtuelle .NET. Toutes les classes de .NET sont disponibles aux langages compatibles .NET ce qui tend à gommer les différences entre langages dans la mesure où les programmes utilisent largement ces classes. Le choix d'un langage .NET devient affaire de goût plus que de performances.

En 2002, C# utilisait la plate-forme .NET 1.0. C# était alors largement une « copie » de Java et .NET une bibliothèque de classes très proche de celle de la plate-forme de développement Java. Dans le cadre de l'apprentissage du langage, on passait d'un environnement C# à un environnement Java sans être vraiment dépaysé. On trouvait même des outils de conversion de code source d'un langage vers l'autre. Depuis, les choses ont évolué. Chaque langage et chaque plate-forme de développement a désormais ses spécificités. Il n'est plus aussi immédiat de transférer ses compétences d'un domaine à l'autre.

C# 3.0 et le framework .NET 3.5 amènent beaucoup de nouveautés. La plus importante est probablement LINQ (Language INtegrated Query) qui permet de requêter de façon uniforme, d'une façon proche de celle du langage SQL, des séquences d'objets provenant de structures en mémoire telles que les tableaux et les listes, de bases de données (SQL Server uniquement pour le moment - février 2008) ou de fichiers XML.

Ce document n'est pas un cours exhaustif. Par exemple, LINQ n'y est pas abordé. Il est destiné à des personnes connaissant déjà la programmation et qui veulent découvrir les bases de C#. Il est une révision du document originel paru en 2002.

Plusieurs livres m'ont aidé à écrire ce cours :

Pour la version 2002 :

  • Professional C# programming, Editions Wrox
  • C# et .NET, Gérard Leblanc, Editions Eyrolles

A l'époque j'avais trouvé excellents ces deux ouvrages. Depuis Gérard Leblanc a publié des versions mises à jour dont la suivante :

  • C# et .NET 2005, Gérard Leblanc, Editions Eyrolles

Pour la révision de 2008, j'ai utilisé les sources suivantes :

  • le document initial de 2002. Ce document issu d'un copier / coller de mon cours Java comportait à la fois des erreurs de typographie et des erreurs plus sérieuses telles que dire que les types primitifs comme System.Int32 étaient des classes alors que ce sont des structures. Mea culpa…
  • la documentation MSDN de Visual Studio Express 2008
  • le livre C# 3.0 in a Nutshell de Joseph et Ben Albahari aux éditions O'Reilly, l'un des meilleurs livres de programmation qu'il m'ait été donné de lire.

Les codes source des exemples de ce document sont disponibles sur le site [http://tahe.developpez.com/dotnet/csharp/].

Serge Tahé, mai 2008

Depuis mai 2008, C# a bien sûr évolué. Ce document reste cependant toujours d'actualité pour l'apprentissage de ce langage. En complément de ce cours, on trouvera une présentation de l'ORM (Object Relational Mapper) Entity Framework dans l'article " Introduction à l'ORM Entity Framework 5 Code First " à l'URL [ http://tahe.developpez.com/dotnet/ef5cf/].

Serge Tahé, octobre 2013


CSharp tutoriel 2008

Apprentissage du langage C#

  • III. Installation de Visual C# 2008

III. Installation de Visual C# 2008

Fin janvier 2008, les versions Express de Visual Studio 2008 sont téléchargeables [2] à l'adresse suivante [1] : [http://msdn2.microsoft.com/fr-fr/express/future/default(en-us).aspx] :

Image non disponible
  • [1] : l'adresse de téléchargement
  • [2] : l'onglet des téléchargements
  • [3] : télécharger C# 2008

L'installation de C# 2008 entraînera celle d'autres éléments :

  • le framework .NET 3.5
  • le SGBD SQL Server Compact 3.5
  • la documentation MSDN

Pour créer un premier programme avec C# 2008, on pourra procéder comme suit, après avoir lancé C# :

Image non disponible
  • [1] : prendre l'option File / New Project
  • [2] : choisir une application de type Console
  • [3] : donner un nom au projet - il va être changé ci-dessous
  • [4] : valider
  • [4b] : le projet créé
  • [4c] : Program.cs est le programme C# généré par défaut dans le projet.
Image non disponible
  • la 1re étape n'a pas demandé où placer le projet. Si on ne fait rien, il sera sauvegardé à un endroit par défaut qui ne nous conviendra probablement pas. L'option [5] permet de sauvegarder le projet dans un dossier précis.
  • on peut donner un nouveau nom au projet dans [6], préciser dans [7] son dossier. Pour cela on peut utiliser [8]. Ici, le projet sera finalement dans le dossier [C:\temp\08-01-31\MyApplication1].
  • en cochant [9], on peut créer un dossier pour la solution nommée dans [10]. Si Solution1 est le nom de la solution :
  • un dossier [C:\temp\08-01-31\Solution1] sera créé pour la solution Solution1
  • un dossier [C:\temp\08-01-31\Solution1\MyApplication1] sera créé pour le projet MyApplication1 . Cette solution convient bien aux solutions composées de plusieurs projets. Chaque projet aura un sous-dossier dans le dossier de la solution.
Image non disponible
  • en [1] : le dossier windows du projet MyApplication1
  • en [2] : son contenu
  • en [3] : le projet dans l'explorateur de projets de Visual Studio

Modifions le code du fichier [Program.cs] [3] comme suit :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace ConsoleApplication1 {
  4.     class Program {
  5.         static void Main(string[] args) {
  6.             Console.WriteLine("1er essai avec C# 2008");
  7.         }
  8.     }
  9. }
  • ligne 3 : l'espace de noms de la classe définie ligne 4. Ainsi le nom complet de la classe, définie ligne 4, est-il ici ConsoleApplication1.Program .
  • lignes 5-7 : la méthode statique Main qui est exécutée lorsqu'on demande l'exécution d'une classe
  • ligne 6 : un affichage écran
Image non disponible

Le programme peut être exécuté de la façon suivante :

  • [Ctrl-F5] pour exécuter le projet, en [1]
  • en [2], l'affichage console obtenu.

L'exécution a rajouté des fichiers au projet :

Image non disponible
  • en [1], on fait afficher tous les fichiers du projet
  • en [2] : le dossier [Release] contient l'exécutable [MyApplication1.exe] du projet.
  • en [3] : le dossier [Debug] qui contiendrait lui aussi un exécutable [MyApplication1.exe] du projet si on avait exécuté le projet en mode [Debug] (touche F5 au lieu de Ctrl-F5). Ce n'est pas le même exécutable que celui obtenu en mode [Release]. Il contient des informations complémentaires permettant au processus de débogage d'avoir lieu.

On peut rajouter un nouveau projet à la solution courante :

Image non disponible
  • [1] : clic droit sur la solution (pas le projet) / Add / New Project
  • [2] : choix d'un type d'application
  • [3] : le dossier proposé par défaut est celui contenant le dossier du projet déjà existant [MyApplication1]
  • [4] : donner un nom au nouveau projet

La solution a alors deux projets :

Image non disponible
  • [1] : le nouveau projet
  • [2] : lorsque la solution est exécutée par (F5 ou Ctrl-F5), l'un des projets est exécuté. C'est celui qu'on désigne par [2].

Un projet peut avoir plusieurs classes exécutables (contenant une méthode Main). Dans ce cas, on doit préciser la classe à exécuter lorsque le projet est exécuté :

Image non disponible
  • [1, 2] : on copie / colle le fichier [Program.cs]
  • [3] : le résultat du copier / coller
  • [4,5] : on renomme les deux fichiers
Image non disponible

La classe P1 (ligne 4) :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace MyApplication2 {
  4.     class P1 {
  5.         static void Main(string[] args) {
  6.         }
  7.     }
  8. }

La classe P2 (ligne 4) :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace MyApplication2 {
  4.     class P2 {
  5.         static void Main(string[] args) {
  6.         }
  7.     }
  8. }

Le projet [MyApplication2] a maintenant deux classes ayant une méthode statique Main . Il faut indiquer au projet laquelle exécuter :

Image non disponible
  • en [1] : les propriétés du projet [MyApplication2]
  • en [2] : le choix de la classe à exécuter lorsque le projet est exécuté (F5 ou Ctrl-F5)
  • en [3] : le type d'exécutable produit - ici une application Console produira un fichier .exe.
  • en [4] : le nom de l'exécutable produit (sans le .exe)
  • en [5] : l'espace de noms par défaut. C'est celui qui sera généré dans le code de chaque nouvelle classe ajoutée au projet. Il peut alors être changé directement dans le code, si besoin est.


CSharp tutoriel 2008

Apprentissage du langage C#

  • IV. Les bases du langage C#
    • IV-A. Introduction
    • IV-B. Les données de C#
    • IV-C. Les types de données prédéfinis
      • IV-C-1. Notation des données littérales
      • IV-C-2. Déclaration des données
        • IV-C-2-a. Rôle des déclarations
        • IV-C-2-b. Déclaration des constantes
        • IV-C-2-c. Déclaration des variables
      • IV-C-3. Les conversions entre nombres et chaînes de caractères
      • IV-C-4. Les tableaux de données
    • IV-D. Les instructions élémentaires de C#
      • IV-D-1. Ecriture sur écran
      • IV-D-2. Lecture de données tapées au clavier
      • IV-D-3. Exemple d'entrées-sorties
      • IV-D-4. Redirection des E/S
      • IV-D-5. Affectation de la valeur d'une expression à une variable
        • IV-D-5-a. Interprétation de l'opération d'affectation
        • IV-D-5-b. Expression arithmétique
        • IV-D-5-c. Priorités dans l'évaluation des expressions arithmétiques
        • IV-D-5-d. Expressions relationnelles
        • IV-D-5-e. Expressions booléennes
        • IV-D-5-f. Traitement de bits
        • IV-D-5-g. Combinaison d'opérateurs
        • IV-D-5-h. Opérateurs d'incrémentation et de décrémentation
        • IV-D-5-i. L'opérateur ternaire ?
        • IV-D-5-j. Priorité générale des opérateurs
        • IV-D-5-k. Les changements de type
    • IV-E. Les instructions de contrôle du déroulement du programme
      • IV-E-1. Arrêt
      • IV-E-2. Structure de choix simple
      • IV-E-3. Structure de cas
      • IV-E-4. Structures de répétition
        • IV-E-4-a. Nombre de répétitions connu
        • IV-E-4-b. Nombre de répétitions inconnu
        • IV-E-4-c. Instructions de gestion de boucle
    • IV-F. La gestion des exceptions
    • IV-G. Application exemple - version 1
    • IV-H. Arguments du programme principal
    • IV-I. Les énumérations
    • IV-J. Passage de paramètres à une fonction
      • IV-J-1. Passage par valeur
      • IV-J-2. Passage par référence
      • IV-J-3. Passage par référence avec le mot clé out

IV. Les bases du langage C#

IV-A. Introduction

Nous traitons C# d'abord comme un langage de programmation classique. Nous aborderons les classes ultérieurement. Dans un programme on trouve deux choses :

  • des données
  • les instructions qui les manipulent

On s'efforce généralement de séparer les données des instructions :

Image non disponible

IV-B. Les données de C#

C# utilise les types de données suivants:

  • les nombres entiers
  • les nombres réels
  • les nombres décimaux
  • les caractères et chaînes de caractères
  • les booléens
  • les objets

IV-C. Les types de données prédéfinis

Type C# | Type .NET | Donnée représentée | Suffixe des valeurs littérales | Codage | Domaine de valeurs
char | Char (S) | caractère |  | 2 octets | caractère Unicode (UTF-16)
string | String (C) | chaîne de caractères |  |  | référence sur une séquence de caractères Unicode
int | Int32 (S) | nombre entier |  | 4 octets | [-231, 231-1] [-2147483648, 2147483647]
uint | UInt32 (S) | .. | U | 4 octets | [0, 232-1] [0, 4294967295]
long | Int64 (S) | .. | L | 8 octets | [-263, 263 -1] [-9223372036854775808, 9223372036854775807]
ulong | UInt64 (S) | .. | UL | 8 octets | [0, 264 -1] [0, 18446744073709551615]
sbyte |  | .. |  | 1 octet | [-27 , 27 -1] [-128,+127]
byte | Byte (S) | .. |  | 1 octet | [0 , 28 -1] [0,255]
short | Int16 (S) | .. |  | 2 octets | [-215, 215-1] [-32768, 32767]
ushort | UInt16 (S) | .. |  | 2 octets | [0, 216-1] [0,65535]
float | Single (S) | nombre réel | F | 4 octets | [1.5 10-45, 3.4 10+38] en valeur absolue
double | Double (S) | .. | D | 8 octets | [-1.7 10+308, 1.7 10+308] en valeur absolue
decimal | Decimal (S) | nombre décimal | M | 16 octets | [1.0 10-28,7.9 10+28] en valeur absolue avec 28 chiffres significatifs
bool | Boolean (S) | .. |  | 1 octet | true, false
object | Object (C) | référence d'objet |  |  | référence d'objet

Ci-dessus, on a mis en face des types C#, leur type .NET équivalent avec le commentaire (S) si ce type est une structure et (C) si le type est une classe. On découvre qu'il y a deux types possibles pour un entier sur 32 bits : int et Int32 . Le type int est un type C#. Int32 est une structure appartenant à l'espace de noms System . Son nom complet est ainsi System.Int32 . Le type int est un alias C# qui désigne la structure .NET System.Int32 . De même, le type C# string est un alias pour le type .NET System.String . System.String est une classe et non une structure. Les deux notions sont proches avec cependant la différence fondamentale suivante :

  • une variable de type Structure se manipule via sa valeur
  • une variable de type Classe se manipule via son adresse (référence en langage objet).

Une structure comme une classe sont des types complexes ayant des attributs et des méthodes. Ainsi, on pourra écrire :

Cacher Sélectionnez
  1. string nomDuType=3.GetType().FullName;

Ci-dessus le littéral 3 est par défaut de type C# int , donc de type .NET System.Int32 . Cette structure a une méthode GetType () qui rend un objet encapsulant les caractéristiques du type de données System.Int32 . Parmi celles-ci, la propriété FullName rend le nom complet du type. On voit donc que le littéral 3 est un objet plus complexe qu'il n'y paraît à première vue.

Voici un programme illustrant ces différents points :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap1 {
  4.     class P00 {
  5.         static void Main(string[] args) {
  6.             // exemple 1
  7.             int ent = 2;
  8.             float fl = 10.5F;
  9.             double d = -4.6;
  10.             string s = "essai";
  11.             uint ui = 5;
  12.             long l = 1000;
  13.             ulong ul = 1001;
  14.             byte octet = 5;
  15.             short sh = -4;
  16.             ushort ush = 10;
  17.             decimal dec = 10.67M;
  18.             bool b = true;
  19.             Console.WriteLine("Type de ent[{1}] : [{0},{2}]", ent.GetType().FullName, ent,sizeof(int));
  20.             Console.WriteLine("Type de fl[{1}]: [{0},{2}]", fl.GetType().FullName, fl, sizeof(float));
  21.             Console.WriteLine("Type de d[{1}] : [{0},{2}]", d.GetType().FullName, d, sizeof(double));
  22.             Console.WriteLine("Type de s[{1}] : [{0}]", s.GetType().FullName, s);
  23.             Console.WriteLine("Type de ui[{1}] : [{0},{2}]", ui.GetType().FullName, ui, sizeof(uint));
  24.             Console.WriteLine("Type de l[{1}] : [{0},{2}]", l.GetType().FullName, l, sizeof(long));
  25.             Console.WriteLine("Type de ul[{1}] : [{0},{2}]", ul.GetType().FullName, ul, sizeof(ulong));
  26.             Console.WriteLine("Type de b[{1}] : [{0},{2}]", octet.GetType().FullName, octet, sizeof(byte));
  27.             Console.WriteLine("Type de sh[{1}] : [{0},{2}]", sh.GetType().FullName, sh, sizeof(short));
  28.             Console.WriteLine("Type de ush[{1}] : [{0},{2}]", ush.GetType().FullName, ush, sizeof(ushort));
  29.             Console.WriteLine("Type de dec[{1}] : [{0},{2}]", dec.GetType().FullName, dec, sizeof(decimal));
  30.             Console.WriteLine("Type de b[{1}] : [{0},{2}]", b.GetType().FullName, b, sizeof(bool));
  31.         }
  32.     }
  33. }
  • ligne 7 : déclaration d'un entier ent
  • ligne 19 : le type d'une variable v peut être obtenue par v.GetType().FullName . La taille d'une structure S peut être obtenue par sizeof(S) . L'instruction Console.WriteLine("… {0} … {1} …",param0, param1…) écrit à l'écran, le texte qui est son premier paramètre, en remplaçant chaque notation {i} par la valeur de l'expression parami .
  • ligne 22 : le type string désignant une classe et non une structure, on ne peut utiliser l'opérateur sizeof .

Voici le résultat de l'exécution :

Cacher Sélectionnez
  1. Type de ent[2] : [System.Int32,4]
  2. Type de fl[10,5]: [System.Single,4]
  3. Type de d[-4,6] : [System.Double,8]
  4. Type de s[essai] : [System.String]
  5. Type de ui[5] : [System.UInt32,4]
  6. Type de l[1000] : [System.Int64,8]
  7. Type de ul[1001] : [System.UInt64,8]
  8. Type de b[5] : [System.Byte,1]
  9. Type de sh[-4] : [System.Int16,2]
  10. Type de ush[10] : [System.UInt16,2]
  11. Type de dec[10,67] : [System.Decimal,16]
  12. Type de b[True] : [System.Boolean,1]

L'affichage produit les types .NET et non les alias C#.

IV-C-1. Notation des données littérales

entier int (32 bits) | 145, -7, 0xFF (hexadécimal)
entier long (64 bits) - suffixe L | 100000L
réel double | 134.789, -45E-18 (-45 10-18)
réel float (suffixe F) | 134.789F, -45E-18F (-45 10-18)
réel decimal (suffixe M) | 100000M
caractère char | 'A', 'b'
chaîne de caractères string | "aujourd'hui" "c:\\chap1\\paragraph3" @"c:\chap1\paragraph3"
booléen bool | true, false
date | new DateTime(1954,10,13) (an, mois, jour) pour le 13/10/1954

On notera les deux chaînes littérales : "c:\\chap1\\paragraph3" et @"c:\chap1\paragraph3" . Dans les chaînes littérales, le caractère \ est interprété. Ainsi "\n" représente la marque de fin de ligne et non la succession des deux caractères \ et n. Si on voulait cette succession, il faudrait écrire "\\n" où la séquence \\ est interprétée comme un seul caractère \ . On pourrait écrire aussi @"\n" pour avoir le même résultat. La syntaxe @"texte" demande que texte soit pris exactement comme il est écrit. On appelle parfois cela une chaîne verbatim.

IV-C-2. Déclaration des données

IV-C-2-a. Rôle des déclarations

Un programme manipule des données caractérisées par un nom et un type. Ces données sont stockées en mémoire. Au moment de la traduction du programme, le compilateur affecte à chaque donnée un emplacement en mémoire caractérisé par une adresse et une taille. Il le fait en s'aidant des déclarations faites par le programmeur.

Par ailleurs celles-ci permettent au compilateur de détecter des erreurs de programmation. Ainsi l'opération

x=x*2;

sera déclarée erronée si x est une chaîne de caractères par exemple.

IV-C-2-b. Déclaration des constantes

La syntaxe de déclaration d'une constante est la suivante :

Cacher Sélectionnez
  1.     const type nom=valeur;          //définit constante nom=valeur

Par exemple :

Cacher Sélectionnez
  1. const float myPI=3.141592F;

Pourquoi déclarer des constantes ?

  • La lecture du programme sera plus aisée si l'on a donné à la constante un nom significatif :
Cacher Sélectionnez
  1.     const  float taux_tva=0.186F;
  • La modification du programme sera plus aisée si la "constante" vient à changer. Ainsi dans le cas précédent, si le taux de tva passe à 33%, la seule modification à faire sera de modifier l'instruction définissant sa valeur :
Cacher Sélectionnez
  1.     const float taux_tva=0.33F;

Si l'on avait utilisé 0.186 explicitement dans le programme, ce serait alors de nombreuses instructions qu'il faudrait modifier.

IV-C-2-c. Déclaration des variables

Une variable est identifiée par un nom et se rapporte à un type de données. C# fait la différence entre majuscules et minuscules. Ainsi les variables FIN et fin sont différentes.

Les variables peuvent être initialisées lors de leur déclaration. La syntaxe de déclaration d'une ou plusieurs variables est :

Cacher Sélectionnez
  1. Identificateur_de_type variable1[=valeur1],variable2=[valeur2],...;

où Identificateur_de_type est un type prédéfini ou bien un type défini par le programmeur. De façon facultative, une variable peut être initialisée en même temps que déclarée.

On peut également ne pas préciser le type exact d'une variable en utilisant le mot clé var en lieu et place de Identificateur_de_type :

Cacher Sélectionnez
  1. var variable1=valeur1,variable2=valeur2,...;

Le mot clé var ne veut pas dire que les variables n'ont pas un type précis. La variable variablei a le type de la donnée valeuri qui lui est affectée. L'initialisation est ici obligatoire afin que le compilateur puisse en déduire le type de la variable.

Voici un exemple :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap1 {
  4.     class P00 {
  5.         static void Main(string[] args) {
  6.             int i=2;
  7.             Console.WriteLine("Type de int i=2 : {0},{1}",i.GetType().Name,i.GetType().FullName);
  8.             var j = 3;
  9.             Console.WriteLine("Type de var j=3 : {0},{1}", j.GetType().Name, j.GetType().FullName);
  10.             var aujourdhui = DateTime.Now;
  11.             Console.WriteLine("Type de var aujourdhui : {0},{1}", aujourdhui.GetType().Name, aujourdhui.GetType().FullName);
  12.         }
  13.     }
  14. }
  • ligne 6 : une donnée typée explicitement
  • ligne 7 : (donnée).GetType().Name est le nom court de (donnée), (donnée).GetType().FullName est le nom complet de (donnée)
  • ligne 8 : une donnée typée implicitement. Parce que 3 est de type int , j sera de type int .
  • ligne 10 : une donnée typée implicitement. Parce que DateTime.Now est de type DateTime , aujourdhui sera de type DateTime .

A l'exécution, on obtient le résultat suivant :

Cacher Sélectionnez
  1. Type de int i=2 : Int32,System.Int32
  2. Type de var j=3 : Int32,System.Int32
  3. Type de var aujourdhui : DateTime,System.DateTime

Une variable typée implicitement par le mot clé var ne peut pas ensuite changer de type. Ainsi, on ne pourrait écrire après la ligne 10 du code, la ligne :

Cacher Sélectionnez
  1.             var aujourdhui = "aujourd'hui";

Nous verrons ultérieurement qu'il est possible de déclarer un type "à la volée" dans une expression. C'est alors un type anonyme, un type auquel l'utilisateur n'a pas donné de nom. C'est le compilateur qui donnera un nom à ce nouveau type. Si une donnée de type anonyme doit être affectée à une variable, la seule façon de déclarer celle-ci est d'utiliser le mot clé var.

IV-C-3. Les conversions entre nombres et chaînes de caractères

nombre -> chaîne | nombre.ToString()
chaine -> int | int.Parse(chaine) ou System.Int32.Parse
chaîne -> long | long.Parse(chaine) ou System.Int64.Parse
chaîne -> double | double.Parse(chaîne) ou System.Double.Parse(chaîne)
chaîne -> float | float.Parse(chaîne) ou System.Float.Parse(chaîne)

La conversion d'une chaîne vers un nombre peut échouer si la chaîne ne représente pas un nombre valide. Il y a alors génération d'une erreur fatale appelée exception. Cette erreur peut être gérée par la clause try/catch suivante :

Cacher Sélectionnez
try{
    appel de la fonction susceptible de générer l'exception
} catch (Exception e){
    traiter l'exception e
}
instruction suivante

Si la fonction ne génère pas d'exception, on passe alors à instruction suivante, sinon on passe dans le corps de la clause catch puis à instruction suivante. Nous reviendrons ultérieurement sur la gestion des exceptions. Voici un programme présentant quelques techniques de conversion entre nombres et chaînes de caractères. Dans cet exemple la fonction affiche écrit à l'écran la valeur de son paramètre. Ainsi affiche(S) écrit la valeur de S à l'écran où S est de type string .

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap1 {
  4.     class P01 {
  5.         static void Main(string[] args) {
  6.
  7.             // données
  8.             const int i = 10;
  9.             const long l = 100000;
  10.             const float f = 45.78F;
  11.             double d = -14.98;
  12.
  13.             // nombre --> chaîne
  14.             affiche(i.ToString());
  15.             affiche(l.ToString());
  16.             affiche(f.ToString());
  17.             affiche(d.ToString());
  18.
  19.             //boolean --> chaîne
  20.             const bool b = false;
  21.             affiche(b.ToString());
  22.
  23.             // chaîne --> int
  24.             int i1;
  25.             i1 = int.Parse("10");
  26.             affiche(i1.ToString());
  27.             try {
  28.                 i1 = int.Parse("10.67");
  29.                 affiche(i1.ToString());
  30.             } catch (Exception e) {
  31.                 affiche("Erreur : " + e.Message);
  32.             }
  33.
  34.             // chaîne --> long
  35.             long l1;
  36.             l1 = long.Parse("100");
  37.             affiche(l1.ToString());
  38.             try {
  39.                 l1 = long.Parse("10.675");
  40.                 affiche(l1.ToString());
  41.             } catch (Exception e) {
  42.                 affiche("Erreur : " + e.Message);
  43.             }
  44.
  45.             // chaîne --> double
  46.             double d1;
  47.             d1 = double.Parse("100,87");
  48.             affiche(d1.ToString());
  49.             try {
  50.                 d1 = double.Parse("abcd");
  51.                 affiche(d1.ToString());
  52.             } catch (Exception e) {
  53.                 affiche("Erreur : " + e.Message);
  54.             }
  55.
  56.             // chaîne --> float
  57.             float f1;
  58.             f1 = float.Parse("100,87");
  59.             affiche(f1.ToString());
  60.             try {
  61.                 d1 = float.Parse("abcd");
  62.                 affiche(f1.ToString());
  63.             } catch (Exception e) {
  64.                 affiche("Erreur : " + e.Message);
  65.             }
  66.
  67.         }// fin main
  68.
  69.         public static void affiche(string S) {
  70.             Console.Out.WriteLine("S={0}",S);
  71.         }
  72.     }// fin classe
  73. }

Lignes 30-32, on gère l'éventuelle exception qui peut se produire. e.Message est le message d'erreur lié à l'exception e .

Les résultats obtenus sont les suivants :

Cacher Sélectionnez
  1. S=10
  2. S=100000
  3. S=45,78
  4. S=-14,98
  5. S=False
  6. S=10
  7. S=Erreur : Input string was not in a correct format.
  8. S=100
  9. S=Erreur : Input string was not in a correct format.
  10. S=100,87
  11. S=Erreur : Input string was not in a correct format.
  12. S=100,87
  13. S=Erreur : Input string was not in a correct format.

On remarquera que les nombres réels sous forme de chaîne de caractères doivent utiliser la virgule et non le point décimal. Ainsi on écrira

Cacher Sélectionnez
  1. double d1=10.7;

mais

Cacher Sélectionnez
  1. double d2=int.Parse("10,7");

IV-C-4. Les tableaux de données

Un tableau C# est un objet permettant de rassembler sous un même identificateur des données de même type. Sa déclaration est la suivante :

Type[] tableau[]=new Type[n]

n est le nombre de données que peut contenir le tableau. La syntaxe Tableau[i] désigne la donnée n° i où i appartient à l'intervalle [0,n-1] . Toute référence à la donnée Tableau[i] où i n'appartient pas à l'intervalle [0,n-1] provoquera une exception. Un tableau peut être initialisé en même temps que déclaré :

Cacher Sélectionnez
  1.     int[] entiers=new int[] {0,10,20,30};

ou plus simplement :

Cacher Sélectionnez
  1.     int[] entiers={0,10,20,30};

Les tableaux ont une propriété Length qui est le nombre d'éléments du tableau.

Un tableau à deux dimensions pourra être déclaré comme suit :

Type[,] tableau=new Type[n,m];

où n est le nombre de lignes, m le nombre de colonnes. La syntaxe Tableau[i,j] désigne l'élément j de la ligne i de t ableau . Le tableau à deux dimensions peut lui aussi être initialisé en même temps qu'il est déclaré :

Cacher Sélectionnez
  1.     double[,] réels=new double[,] { {0.5, 1.7}, {8.4, -6}};

ou plus simplement :

Cacher Sélectionnez
  1.     double[,] réels={ {0.5, 1.7}, {8.4, -6}};

Le nombre d'éléments dans chacune des dimensions peut être obtenue par la méthode GetLength(i) où i=0 représente la dimension correspondant au 1er indice, i=1 la dimension correspondant au 2ième indice…

Le nombre total de dimensions est obtenu avec la propriété Rank, le nombre total d'éléments avec la propriété Length.

Un tableau de tableaux est déclaré comme suit :

Type[][] tableau=new Type[n][];

La déclaration ci-dessus crée un tableau de n lignes. Chaque élément tableau[i] est une référence de tableau à une dimension. Ces références tableau[i] ne sont pas initialisées lors de la déclaration ci-dessus. Elles ont pour valeur la référence null .

L'exemple ci-dessous illustre la création d'un tableau de tableaux :

Cacher Sélectionnez
  1.             // un tableau de tableaux
  2.             string[][] noms = new string[3][];
  3.             for (int i = 0; i < noms.Length; i++) {
  4.                 noms[i] = new string[i + 1];
  5.             }//for
  6.             // initialisation
  7.             for (int i = 0; i < noms.Length; i++) {
  8.                 for (int j = 0; j < noms[i].Length; j++) {
  9.                     noms[i][j] = "nom" + i + j;
  10.                 }//for j
  11.             }//for i
  • ligne 2 : un tableau noms de 3 éléments de type string[][] . Chaque élément est un pointeur de tableau (une référence d'objet) dont les éléments sont de type string[] .
  • lignes 3-5 : les 3 éléments du tableau noms sont initialisés. Chacun "pointe" désormais sur un tableau d'éléments de type string[] . noms[i][j] est l'élément j du tableau de type string [] référencé par noms[i] .
  • ligne 9 : initialisation de l'éélment noms[i][j] à l'intérieur d'une double boucle. Ici noms[i] est un tableau de i+1 éléments. Comme noms[i] est un tableau, noms[i].Length est son nombre d'éléments.

Voici un exemple regroupant les trois types de tableaux que nous venons de présenter :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap1 {
  4.     // tableaux
  5.
  6.     using System;
  7.
  8.     // classe de test
  9.     public class P02 {
  10.         public static void Main() {
  11.             // un tableau à 1 dimension initialisé
  12.             int[] entiers = new int[] { 0, 10, 20, 30 };
  13.             for (int i = 0; i < entiers.Length; i++) {
  14.                 Console.Out.WriteLine("entiers[{0}]={1}", i, entiers[i]);
  15.             }//for

  16.
  17.             // un tableau à 2 dimensions initialisé
  18.             double[,] réels = new double[,] { { 0.5, 1.7 }, { 8.4, -6 } };
  19.             for (int i = 0; i < réels.GetLength(0); i++) {
  20.                 for (int j = 0; j < réels.GetLength(1); j++) {
  21.                     Console.Out.WriteLine("réels[{0},{1}]={2}", i, j, réels[i, j]);
  22.                 }//for j
  23.             }//for i
  24.
  25.             // un tableau de tableaux
  26.             string[][] noms = new string[3][];
  27.             for (int i = 0; i < noms.Length; i++) {
  28.                 noms[i] = new string[i + 1];
  29.             }//for
  30.             // initialisation
  31.             for (int i = 0; i < noms.Length; i++) {
  32.                 for (int j = 0; j < noms[i].Length; j++) {
  33.                     noms[i][j] = "nom" + i + j;
  34.                 }//for j
  35.             }//for i
  36.             // affichage
  37.             for (int i = 0; i < noms.Length; i++) {
  38.                 for (int j = 0; j < noms[i].Length; j++) {
  39.                     Console.Out.WriteLine("noms[{0}][{1}]={2}", i, j, noms[i][j]);
  40.                 }//for j
  41.             }//for i
  42.         }//Main
  43.     }//class
  44. }//namespace

A l'exécution, nous obtenons les résultats suivants :

Cacher Sélectionnez
  1. entiers[0]=0
  2. entiers[1]=10
  3. entiers[2]=20
  4. entiers[3]=30
  5. réels[0,0]=0,5
  6. réels[0,1]=1,7
  7. réels[1,0]=8,4
  8. réels[1,1]=-6
  9. noms[0][0]=nom00
  10. noms[1][0]=nom10
  11. noms[1][1]=nom11
  12. noms[2][0]=nom20
  13. noms[2][1]=nom21
  14. noms[2][2]=nom22

IV-D. Les instructions élémentaires de C#

On distingue

1les instructions élémentaires exécutées par l'ordinateur.

2les instructions de contrôle du déroulement du programme.

Les instructions élémentaires apparaissent clairement lorsqu'on considère la structure d'un micro-ordinateur et de ses périphériques.

Image non disponible

1. lecture d'informations provenant du clavier

2. traitement d'informations

3. écriture d'informations à l'écran

IV-D-1. Ecriture sur écran

Il existe différentes instructions d'écriture à l'écran :

Console.Out.WriteLine(expression)

Console.WriteLine(expression)

Console.Error.WriteLine (expression)

où expression est tout type de donnée qui puisse être converti en chaîne de caractères pour être affiché à l'écran. Tous les objets de C# ou .NET ont une méthode ToString() qui est utilisée pour faire cette conversion.

La classe System.Console donne accès aux opérations d'écriture écran (Write, WriteLine). La classe Console a deux propriétés Out et Error qui sont des flux d'écriture de type TextWriter :

  • Console.WriteLine() est équivalent à Console.Out.WriteLine() et écrit sur le flux Out associé habituellement à l'écran.
  • Console.Error.WriteLine() écrit sur le flux Error, habituellement associé lui aussi à l'écran.

Les flux Out et Error peuvent être redirigés vers des fichiers texte au moment de l'exécution du programme comme nous le verrons prochainement.

IV-D-2. Lecture de données tapées au clavier

Le flux de données provenant du clavier est désigné par l'objet Console.In de type TextReader . Ce type d'objets permet de lire une ligne de texte avec la méthode ReadLine :

Cacher Sélectionnez
  1.     string ligne=Console.In.ReadLine();

La classe Console offre une méthode ReadLine associée par défaut au flux In . On peut donc écrire écrire :

Cacher Sélectionnez
  1.     string ligne=Console.ReadLine();

La ligne tapée au clavier est rangée dans la variable ligne et peut ensuite être exploitée par le programme. Le flux In peut être redirigé vers un fichier, comme les flux Out et Error.

IV-D-3. Exemple d'entrées-sorties

Voici un court programme d'illustration des opérations d'entrées-sorties clavier/écran :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap1 {
  4.     // classe de test
  5.     public class P03 {
  6.         public static void Main() {
  7.
  8.             // écriture sur le flux Out
  9.             object obj = new object();
  10.             Console.Out.WriteLine(obj);
  11.
  12.             // écriture sur le flux Error
  13.             int i = 10;
  14.             Console.Error.WriteLine("i=" + i);
  15.
  16.             // lecture d'une ligne saisie au clavier
  17.             Console.Write("Tapez une ligne : ");
  18.             string ligne = Console.ReadLine();
  19.             Console.WriteLine("ligne={0}", ligne);
  20.         }//fin main
  21.     }//fin classe
  22. }
  • ligne 9 : obj est une référence d'objet
  • ligne 10 : obj est écrit à l'écran. Par défaut, c'est la méthode obj.ToString() qui est appelée.
  • ligne 14 : on peut aussi écrire :
Cacher Sélectionnez
  1.             Console.Error.WriteLine("i={0}",i);

Le 1er paramètre "i={0}" est le format d'affichage, les autres paramètres, les expressions à afficher. Les éléments {n} sont des paramètres "positionnels". A l'exécution, le paramètre {n} est remplacé par la valeur de l'expression n° n.

Le résultat de l'exécution est le suivant :

Cacher Sélectionnez
  1. System.Object
  2. i=10
  3. Tapez une ligne : je suis là
  4. ligne=je suis là
  • ligne 1 : l'affichage produit par la ligne 10 du code. La méthode obj.ToString() a affiché le nom du type de la variable obj : System.Object . Le type object est un alias C# du type .NET System.Object .

IV-D-4. Redirection des E/S

Il existe sous DOS et UNIX trois périphériques standard appelés :

  • périphérique d'entrée standard - désigne par défaut le clavier et porte le n° 0
  • périphérique de sortie standard - désigne par défaut l'écran et porte le n° 1
  • périphérique d'erreur standard - désigne par défaut l'écran et porte le n° 2

En C#, le flux d'écriture Console.Out écrit sur le périphérique 1, le flux d'écriture Console.Error écrit sur le périphérique 2 et le flux de lecture Console.In lit les données provenant du périphérique 0.

Lorsqu'on lance un programme sous Dos ou Unix, on peut fixer quels seront les périphériques 0, 1 et 2 pour le programme exécuté. Considérons la ligne de commande suivante :

Cacher Sélectionnez
  1. pg arg1 arg2 .. argn

Derrière les arguments argi du programme pg , on peut rediriger les périphériques d'E/S standard vers des fichiers:

0<in.txt | le flux d'entrée standard n° 0 est redirigé vers le fichier in.txt . Dans le programme le flux Console.In prendra donc ses données dans le fichier in.txt .
1>out.txt | redirige la sortie n° 1 vers le fichier out.txt . Cela entraîne que dans le programme le flux Console.Out écrira ses données dans le fichier out.txt
1>>out.txt | idem, mais les données écrites sont ajoutées au contenu actuel du fichier out.txt .
2>error.txt | redirige la sortie n° 2 vers le fichier error.txt . Cela entraîne que dans le programme le flux Console.Error écrira ses données dans le fichier error.txt
2>>error.txt | idem, mais les données écrites sont ajoutées au contenu actuel du fichier error.txt .
1>out.txt 2>error.txt | Les périphériques 1 et 2 sont tous les deux redirigés vers des fichiers

On notera que pour rediriger les flux d'E/S du programme pg vers des fichiers, le programme pg n'a pas besoin d'être modifié. C'est le système d'exploitation qui fixe la nature des périphériques 0,1 et 2. Considérons le programme suivant :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap1 {
  4.
  5.     // redirections
  6.     public class P04 {
  7.         public static void Main(string[] args) {
  8.             // lecture flux In
  9.             string data = Console.In.ReadLine();
  10.             // écriture flux Out
  11.             Console.Out.WriteLine("écriture dans flux Out : " + data);
  12.             // écriture flux Error
  13.             Console.Error.WriteLine("écriture dans flux Error : " + data);
  14.         }//Main
  15.     }//classe
  16. }

Générons l'exécutable de ce code source :

Image non disponible
  • en [1] : l'exécutable est créé par clic droit sur le projet / Build
  • en [2] : dans une fenêtre Dos, l'exécutable 04.exe a été créé dans le dossier bin/Release du projet.

Emettons les commandes suivantes dans la fenêtre Dos [2] :

Cacher Sélectionnez
  1. ...\04\bin\Release>echo test >in.txt
  2. ...\04\bin\Release>more in.txt
  3. test
  4. ...\04\bin\Release>04 0<in.txt 1>out.txt 2>err.txt
  5. ...\04\bin\Release>more out.txt
  6. écriture dans flux Out : test
  7. ...\04\bin\Release>more err.txt
  8. écriture dans flux Error : test
  • ligne 1 : on met la chaîne test dans le fichier in.txt
  • lignes 2-3 : on affiche le contenu du fichier in.txt pour vérification
  • ligne 4 : exécution du programme 04.exe . Le flux In est redirigé vers le fichier in.txt , le flux Out vers le fichier out.txt , le flux Error vers le fichier err.txt . L'exécution ne provoque aucun affichage.
  • lignes 5-6 : contenu du fichier out.txt . Ce contenu nous montre que :
  • le fichier in.txt a été lu
  • l'affichage écran a été redirigé vers out.txt
  • lignes 7-8 : vérification analogue pour le fichier err.txt

On voit clairement que les flux Out et In n'écrivent pas sur les mêmes périphériques puisqu'on a pu les rediriger séparément.

IV-D-5. Affectation de la valeur d'une expression à une variable

On s'intéresse ici à l'opération variable=expression;

L'expression peut être de type : arithmétique, relationnelle, booléenne, caractères

IV-D-5-a. Interprétation de l'opération d'affectation

L'opération variable=expression;

est elle-même une expression dont l'évaluation se déroule de la façon suivante :

  • la partie droite de l'affectation est évaluée : le résultat est une valeur V.
  • la valeur V est affectée à la variable
  • la valeur V est aussi la valeur de l'affectation vue cette fois en tant qu'expression.

C'est ainsi que l'opération

V1=V2=expression

est légale. A cause de la priorité, c'est l'opérateur = le plus à droite qui va être évalué. On a donc

V1=(V2=expression)

L'expression V2=expression est évaluée et a pour valeur V . L'évaluation de cette expression a provoqué l'affectation de V à V2. L'opérateur = suivant est alors évalué sous la forme :

V1=V

La valeur de cette expression est encore V. Son évaluation provoque l'affectation de V à V1.

Ainsi donc, l'opération V1=V2=expression

est une expression dont l'évaluation

1provoque l'affectation de la valeur de expression aux variables V1 et V2

2rend comme résultat la valeur de expression .

On peut généraliser à une expression du type :

V1=V2=….=Vn=expression

IV-D-5-b. Expression arithmétique

Les opérateurs des expressions arithmétiques sont les suivants :

+addition

-soustraction

*multiplication

/division : le résultat est le quotient exact si l'un au moins des opérandes est réel. Si les deux opérandes sont entiers le résultat est le quotient entier. Ainsi 5/2 -> 2 et 5.0/2 ->2.5.

%division : le résultat est le reste quelque soit la nature des opérandes, le quotient étant lui entier. C'est donc l'opération modulo.

Il existe diverses fonctions mathématiques. En voici quelques-unes :

double Sqrt(double x) | racine carrée
double Cos(double x) | Cosinus
double Sin(double x) | Sinus
double Tan(double x) | Tangente
double Pow(double x,double y) | x à la puissance y (x>0)
double Exp(double x) | Exponentielle
double Log(double x) | Logarithme népérien
double Abs(double x) | valeur absolue

etc…

Toutes ces fonctions sont définies dans une classe C# appelée Math. Lorsqu'on les utilise, il faut les préfixer avec le nom de la classe où elles sont définies. Ainsi on écrira :

Cacher Sélectionnez
  1. double x, y=4;
  2. x=Math.Sqrt(y);

La définition complète de la classe Math est la suivante :

Image non disponible
Image non disponible
Image non disponible
Image non disponible
Image non disponible

IV-D-5-c. Priorités dans l'évaluation des expressions arithmétiques

La priorité des opérateurs lors de l'évaluation d'une expression arithmétique est la suivante (du plus prioritaire au moins prioritaire) :

[fonctions], [ ( )],[ *, /, %], [+, -]

Les opérateurs d'un même bloc [ ] ont même priorité.

IV-D-5-d. Expressions relationnelles

Les opérateurs sont les suivants :

<, <=, ==, !=, >, >=

priorités des opérateurs

  • >, >=, <, <=
  • ==, !=

Le résultat d'une expression relationnelle est le booléen false si expression est fausse, true sinon.

Cacher Sélectionnez
  1.       bool fin;
  2.       int x=...;
  3.       fin=x>4;

Comparaison de deux caractères

Soient deux caractères C1 et C2. Il est possible de les comparer avec les opérateurs

<, <=, ==, !=, >, >=

Ce sont alors leurs codes Unicode, qui sont des nombres, qui sont alors comparés. Selon l'ordre Unicode on a les relations suivantes :

espace < .. < '0' < '1' < .. < '9' < .. < 'A' < 'B' < .. < 'Z' < .. < 'a' < 'b' < .. <'z'

Comparaison de deux chaînes de caractères

Elles sont comparées caractère par caractère. La première inégalité rencontrée entre deux caractères induit une inégalité de même sens sur les chaînes.

Exemples :

Soit à comparer les chaînes "Chat" et "Chien"

"Chat""Chien"

‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑‑

'C'='C'

'h'='h'

'a'<'i'

Cette dernière inégalité permet de dire que "Chat" < "Chien".

Soit à comparer les chaînes "Chat" et "Chaton". Il y a égalité tout le temps jusqu'à épuisement de la chaîne "Chat". Dans ce cas, la chaîne épuisée est déclarée la plus "petite". On a donc la relation

"Chat" < "Chaton".

Fonctions de comparaison de deux chaînes

On peut utiliser les opérateurs relationnels == et != pour tester l'égalité ou non de deux chaînes, ou bien la méthode Equals de la classe System.String . Pour les relations < <= > >=, il faut utiliser la méthode de la classe System.String :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap1 {
  4.     class P05 {
  5.         static void Main(string[] args) {
  6.             string chaine1="chat", chaine2="chien";
  7.             int n = chaine1.CompareTo(chaine2);
  8.             bool egal = chaine1.Equals(chaine2);
  9.             Console.WriteLine("i={0}, egal={1}", n, egal);
  10.             Console.WriteLine("chien==chaine1:{0},chien!=chaine2:{1}", "chien"==chaine1,"chien" != chaine2);
  11.         }
  12.     }
  13. }

Ligne 7, la variable i aura la valeur :

0si les deux chaînes sont égales

1si chaîne n°1 > chaîne n°2

-1si chaîne n°1 < chaîne n°2

Ligne 8, la variable egal aura la valeur true si les deux chaînes sont égales, false sinon. Ligne 10, on utilise les opérateurs == et != pour vérifier l'égalité ou non de deux chaînes.

Les résultats de l'exécution :

Cacher Sélectionnez
  1. i=-1, egal=False
  2. chien==chaine1:False,chien!=chaine2:False

IV-D-5-e. Expressions booléennes

Les opérateurs utilisables sont AND ( && ) OR( || ) NOT ( ! ). Le résultat d'une expression booléenne est un booléen.

priorités des opérateurs :

  • !
  • &&
  • ||
Cacher Sélectionnez
  1.             double x = 3.5;
  2.             bool valide = x > 2 && x < 4;

Les opérateurs relationnels ont priorité sur les opérateurs && et ||.

IV-D-5-f. Traitement de bits

Les opérateurs

Soient i et j deux entiers.

i<<n | décale i de n bits sur la gauche. Les bits entrants sont des zéros.
i>>n | décale i de n bits sur la droite. Si i est un entier signé (signed char, int, long) le bit de signe est préservé.
i & j | fait le ET logique de i et j bit à bit.
i | j | fait le OU logique de i et j bit à bit.
~i | complémente i à 1
i^j | fait le OU EXCLUSIF de i et j

Soit le code suivant :

Cacher Sélectionnez
  1. short i = 100, j = -13;
  2. ushort k = 0xF123;
  3. Console.WriteLine("i=0x{0:x4}, j=0x{1:x4}, k=0x{2:x4}", i,j,k);
  4. Console.WriteLine("i<<4=0x{0:x4}, i>>4=0x{1:x4},k>>4=0x{2:x4},i&j=0x{3:x4},i|j=0x{4:x4},~i=0x{5:x4},j<<2=0x{6:x4},j>>2=0x{7:x4}", i << 4, i >> 4, k >> 4, (short)(i & j), (short)(i | j), (short)(~i), (short)(j << 2), (short)(j >> 2));
  • le format {0:x4} affiche le paramètre n° 0 au format hexadécimal (x) avec 4 caractères (4).

Les résultats de l'exécution sont les suivants :

Cacher Sélectionnez
  1. i=0x0064, j=0xfff3, k=0xf123
  2. i<<4=0x0640, i>>4=0x0006,k>>4=0x0f12,i&j=0x0060,i|j=0xfff7,~i=0xff9b,j<<2=0xffcc,j>>2=0xfffc

IV-D-5-g. Combinaison d'opérateurs

a=a+b peut s'écrire a+=b

a=a-b peut s'écrire a-=b

Il en est de même avec les opérateurs /, %,* ,<<, >>, &, |, ^ . Ainsi a=a/2; peut s'écrire a/=2;

IV-D-5-h. Opérateurs d'incrémentation et de décrémentation

La notation variable++ signifie variable=variable+1 ou encore variable+=1

La notation variable-- signifie variable=variable-1 ou encore variable-=1 .

IV-D-5-i. L'opérateur ternaire ?

L'expression

Cacher Sélectionnez
  1.     expr_cond ? expr1:expr2

est évaluée de la façon suivante :

1l'expression expr_cond est évaluée. C'est une expression conditionnelle à valeur vrai ou faux

2Si elle est vraie, la valeur de l'expression est celle de expr1 et expr2 n'est pas évaluée.

3Si elle est fausse, c'est l'inverse qui se produit : la valeur de l'expression est celle de expr2 et expr1 n'est pas évaluée.

L'opération i=(j>4 ? j+1:j-1); affectera à la variable i : j+1 si j>4 , j-1 sinon. C'est la même chose que d'écrire if(j>4) i=j+1; else i=j-1; mais c'est plus concis.

IV-D-5-j. Priorité générale des opérateurs

() [] fonction | gd
! ~ ++ -- | dg
new (type) opérateurs cast | dg
* / % | gd
+ - | gd
<< >> | gd
< <= > >= instanceof | gd
== != | gd
& | gd
^ | gd
| | gd
&& | gd
|| | gd
? : | dg
= += -= etc. . | dg

gd indique qu'a priorité égale, c'est la priorité gauche-droite qui est observée. Cela signifie que lorsque dans une expression, l'on a des opérateurs de même priorité, c'est l'opérateur le plus à gauche dans l'expression qui est évalué en premier. dg indique une priorité droite-gauche.

IV-D-5-k. Les changements de type

Il est possible, dans une expression, de changer momentanément le codage d'une valeur. On appelle cela changer le type d'une donnée ou en anglais type casting. La syntaxe du changement du type d'une valeur dans une expression est la suivante:

(type) valeur

La valeur prend alors le type indiqué. Cela entraîne un changement de codage de la valeur.

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap1 {
  4.     class P06 {
  5.         static void Main(string[] args) {
  6.             int i = 3, j = 4;
  7.             float f1=i/j;
  8.             float f2=(float)i/j;
  9.             Console.WriteLine("f1={0}, f2={1}",f1,f2);
  10.         }
  11.     }
  12. }
  • ligne 7, f1 aura la valeur 0.0. La division 3/4 est une division entière puisque les deux opérandes sont de type int .
  • ligne 8, (float)i est la valeur de i transformée en float . Maintenant, on a une division entre un réel de type float et un entier de type int . C'est la division entre nombres réels qui est alors faite. La valeur de j sera elle également transformée en type float , puis la division des deux réels sera faite. f2 aura alors la valeur 0,75.

Voici les résultats de l'exécution :

Cacher Sélectionnez
  1. f1=0, f2=0,75

Dans l'opération (float)i :

  • i est une valeur codée de façon exacte sur 2 octets
  • (float) i est la même valeur codée de façon approchée en réel sur 4 octets

Il y a donc transcodage de la valeur de i . Ce transcodage n'a lieu que le temps d'un calcul, la variable i conservant toujours son type int .

IV-E. Les instructions de contrôle du déroulement du programme

IV-E-1. Arrêt

La méthode E xit définie dans la classe Environment permet d'arrêter l'exécution d'un programme.

Cacher Sélectionnez
  1. syntaxe    void Exit(int status)

action arrête le processus en cours et rend la valeur status au processus père

Exit provoque la fin du processus en cours et rend la main au processus appelant. La valeur de status peut être utilisée par celui-ci. Sous DOS, cette variable status est rendue dans la variable système ERRORLEVEL dont la valeur peut être testée dans un fichier batch. Sous Unix, avec l'interpréteur de commandes Shell Bourne, c'est la variable $? qui récupère la valeur de status .

Cacher Sélectionnez
  1.     Environment.Exit(0);

arrêtera l'exécution du programme avec une valeur d'état à 0.

IV-E-2. Structure de choix simple

Cacher Sélectionnez
  1. syntaxe :  if (condition) {actions_condition_vraie;} else {actions_condition_fausse;}

notes :

  • la condition est entourée de parenthèses.
  • chaque action est terminée par point-virgule.
  • les accolades ne sont pas terminées par point-virgule.
  • les accolades ne sont nécessaires que s'il y a plus d'une action.
  • la clause else peut être absente.
  • il n'y a pas de clause then .

L'équivalent algorithmique de cette structure est la structure si .. alors … sinon :

si condition

alors actions_condition_vraie

sinon actions_condition_fausse

finsi

exemple

Cacher Sélectionnez
  1.     if (x>0)  { nx=nx+1;sx=sx+x;} else dx=dx-x;

On peut imbriquer les structures de choix :

Cacher Sélectionnez
  1. if(condition1)
  2. if (condition2)
  3.         {......}
  4.       else         //condition2
  5.          {......}
  6. else         //condition1
  7.      {.......}

Se pose parfois le problème suivant :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap1 {
  4.     class P07 {
  5.         static void Main(string[] args) {
  6.             int n = 5;
  7.             if (n > 1)
  8.                 if (n > 6)
  9.                     Console.Out.WriteLine(">6");
  10.                 else Console.Out.WriteLine("<=6");
  11.         }
  12.     }
  13. }

Dans l'exemple précédent, le else de la ligne 10 se rapporte à quel if ? La règle est qu'un else se rapporte toujours au if le plus proche : if(n>6), ligne 8, dans l'exemple. Considérons un autre exemple :

Cacher Sélectionnez
  1.             if (n2 > 1) {
  2.                 if (n2 > 6) Console.Out.WriteLine(">6");
  3.        } else Console.Out.WriteLine("<=1");

Ici nous voulions mettre un else au if(n2>1) et pas de else au if(n2>6) . A cause de la remarque précédente, nous sommes obligés de mettre des accolades au if(n2>1) {…} else …

IV-E-3. Structure de cas

La syntaxe est la suivante :

Cacher Sélectionnez
  1. switch(expression) {
  2.     case v1:
  3.             actions1;
  4.             break;
  5.     case v2:
  6.             actions2;
  7.             break;
  8.          . .. .. .. .. ..
  9.     default:
  10.             actions_sinon;
  11.             break;
  12. }

notes

  • la valeur de l'expression de contrôle du switch peut être un entier, un caractère, une chaîne de caractères
  • l'expression de contrôle est entourée de parenthèses.
  • la clause default peut être absente.
  • les valeurs vi sont des valeurs possibles de l'expression. Si l'expression a pour valeur vi , les actions derrière la clause case v i sont exécutées.
  • l'instruction break fait sortir de la structure de cas.
  • chaque bloc d'instructions lié à une valeur vi doit se terminer par une instruction de branchement (break, goto, return…) sinon le compilateur signale une erreur.

exemple

En algorithmique

selon la valeur de choix

cas 0

fin du module

cas 1

exécuter module M1

cas 2

exécuter module M2

sinon

erreur<--vrai

findescas

En C#

Cacher Sélectionnez
  1. int choix = 2;
  2.             bool erreur = false;
  3.             switch (choix) {
  4.                 case 0: return;
  5.                 case 1: M1(); break;
  6.                 case 2: M2(); break;
  7.                 default: erreur = true; break;
  8.             }
  9.         }// fin Main
  10.
  11.         static void M1() {
  12.             Console.WriteLine("M1");
  13.         }
  14.
  15.         static void M2() {
  16.             Console.WriteLine("M2");
  17.         }
  18. }

IV-E-4. Structures de répétition

IV-E-4-a. Nombre de répétitions connu

Structure for

La syntaxe est la suivante :

Cacher Sélectionnez
  1.     for (i=id;i<=if;i=i+ip){
  2.        actions;
  3.       }

Notes

  • les 3 arguments du for sont à l'intérieur d'une parenthèse et séparés par des points-virgules.
  • chaque action du for est terminée par un point-virgule.
  • l'accolade n'est nécessaire que s'il y a plus d'une action.
  • l'accolade n'est pas suivie de point-virgule.

L'équivalent algorithmique est la structure pour :

pour i variant de id à if avec un pas de ip

actions

finpour

qu'on peut traduire par une structure tantque :

i  id

tantque i<=if

actions

i i+ip

fintantque

Structure foreach

La syntaxe est la suivante :

Cacher Sélectionnez
  1. foreach (Type variable in collection)
  2.     instructions;
  3. }

Notes

  • collection est une collection d'objets énumérable. La collection d'objets énumérable que nous connaissons déjà est le tableau
  • Type est le type des objets de la collection. Pour un tableau, ce serait le type des éléments du tableau
  • variable est une variable locale à la boucle qui va prendre successivement pour valeur, toutes les valeurs de la collection.

Ainsi le code suivant :

Cacher Sélectionnez
  1.             string[] amis = { "paul", "hélène", "jacques", "sylvie" };
  2.             foreach (string nom in amis) {
  3.                 Console.WriteLine(nom);
  4.          }

afficherait :

Cacher Sélectionnez
  1. paul
  2. hélène
  3. jacques
  4. sylvie

IV-E-4-b. Nombre de répétitions inconnu

Il existe de nombreuses structures en C# pour ce cas.

Structure tantque (while)

Cacher Sélectionnez
  1.     while(condition){
  2.           actions;
  3.         }

On boucle tant que la condition est vérifiée. La boucle peut ne jamais être exécutée.

notes :

  • la condition est entourée de parenthèses.
  • chaque action est terminée par point-virgule.
  • l'accolade n'est nécessaire que s'il y a plus d'une action.
  • l'accolade n'est pas suivie de point-virgule.

La structure algorithmique correspondante est la structure tantque :

tantque condition

actions

fintantque

Structure répéter jusqu'à (do while)

La syntaxe est la suivante :

Cacher Sélectionnez
  1.     do{
  2.        instructions;
  3.     }while(condition);

On boucle jusqu'à ce que la condition devienne fausse. Ici la boucle est faite au moins une fois.

notes

  • la condition est entourée de parenthèses.
  • chaque action est terminée par point-virgule.
  • l'accolade n'est nécessaire que s'il y a plus d'une action.
  • l'accolade n'est pas suivie de point-virgule.

La structure algorithmique correspondante est la structure répéter … jusqu'à :

répéter

actions

jusqu'à condition

Structure pour générale (for)

La syntaxe est la suivante :

Cacher Sélectionnez
  1. for(instructions_départ;condition;instructions_fin_boucle){
  2.     instructions;
  3. }

On boucle tant que la condition est vraie (évaluée avant chaque tour de boucle). Instructions_départ sont effectuées avant d'entrer dans la boucle pour la première fois. Instructions_fin_boucle sont exécutées après chaque tour de boucle.

notes

  • les différentes instructions dans instructions_depart et instructions_fin_boucle sont séparées par des virgules.

La structure algorithmique correspondante est la suivante :

instructions_départ

tantque condition

actions

instructions_fin_boucle

fintantque

Exemples

Les fragments de code suivants calculent tous la somme des 10 premiers nombres entiers.

Cacher Sélectionnez
  1.             int i, somme, n=10;
  2.             for (i = 1, somme = 0; i <= n; i = i + 1)
  3.                 somme = somme + i;
  4.
  5.             for (i = 1, somme = 0; i <= n; somme = somme + i, i = i + 1) ;
  6.
  7.             i = 1; somme = 0;
  8.             while (i <= n) { somme += i; i++; }
  9.
  10.             i = 1; somme = 0;
  11.             do somme += i++;
  12.             while (i <= n);

IV-E-4-c. Instructions de gestion de boucle

break | fait sortir de la boucle for, while, do … while.
continue | fait passer à l'itération suivante des boucles for, while, do … while

IV-F. La gestion des exceptions

De nombreuses fonctions C# sont susceptibles de générer des exceptions, c'est-à-dire des erreurs. Lorsqu'une fonction est susceptible de générer une exception, le programmeur devrait la gérer dans le but d'obtenir des programmes plus résistants aux erreurs : il faut toujours éviter le "plantage" sauvage d'une application.

La gestion d'une exception se fait selon le schéma suivant :

try{

code susceptible de générer une exception

} catch (Exception e){

traiter l'exception e

}

instruction suivante

Si la fonction ne génère pas d'exception, on passe alors à instruction suivante , sinon on passe dans le corps de la clause catch puis à instruction suivante . Notons les points suivants :

  • e est un objet de type Exception ou dérivé. On peut être plus précis en utilisant des types tels que IndexOutOfRangeException, FormatException, SystemException, etc… : il existe plusieurs types d'exceptions. En écrivant catch (Exception e) , on indique qu'on veut gérer toutes les types d'exceptions. Si le code de la clause try est susceptible de générer plusieurs types d'exceptions, on peut vouloir être plus précis en gérant l'exception avec plusieurs clauses catch :

try{

code susceptible de générer les exceptions

} catch ( IndexOutOfRangeException e1){

traiter l'exception e1

}

} catch ( FormatException e2){

traiter l'exception e2

}

instruction suivante

  • On peut ajouter aux clauses try/catch , une clause finally :

try{

code susceptible de générer une exception

} catch (Exception e){

traiter l'exception e

}

finally{

code exécuté après try ou catch

}

instruction suivante

Qu'il y ait exception ou pas, le code de la clause finally sera toujours exécuté.

  • Dans la clause catch , on peut ne pas vouloir utiliser l'objet Exception disponible. Au lieu d'écrire catch (Exception e){..} , on écrit alors catch(Exception){…} ou plus simplement catch {…} .
  • La classe Exception a une propriété Message qui est un message détaillant l'erreur qui s'est produite. Ainsi si on veut afficher celui-ci, on écrira :

catch (Exception ex){

Console.WriteLine("L'erreur suivante s'est produite : {0}",ex.Message);

…

}//catch

  • La classe Exception a une méthode ToString qui rend une chaîne de caractères indiquant le type de l'exception ainsi que la valeur de la propriété Message . On pourra ainsi écrire :

catch (Exception ex){

Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.ToString());

…

}//catch

On peut écrire aussi :

catch (Exception ex){

Console.WriteLine("L'erreur suivante s'est produite : {0}",ex);

…

}//catch

Le compilateur va attribuer au paramètre {0}, la valeur ex.ToString() .

L'exemple suivant montre une exception générée par l'utilisation d'un élément de tableau inexistant :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap1 {
  4.     class P08 {
  5.         static void Main(string[] args) {
  6.             // déclaration & initialisation d'un tableau
  7.             int[] tab = { 0, 1, 2, 3 };
  8.             int i;
  9.             // affichage tableau avec un for
  10.             for (i = 0; i < tab.Length; i++)
  11.                 Console.WriteLine("tab[{0}]={1}", i, tab[i]);
  12.             // affichage tableau avec un for each
  13.             foreach (int élmt in tab) {
  14.                 Console.WriteLine(élmt);
  15.             }
  16.             // génération d'une exception
  17.             try {
  18.                 tab[100] = 6;
  19.             } catch (Exception e) {
  20.                 Console.Error.WriteLine("L'erreur suivante s'est produite : " + e);
  21.                 return;
  22.             }//try-catch
  23.             finally {
  24.                 Console.WriteLine("finally ...");
  25.             }
  26.         }
  27.     }
  28. }

Ci-dessus, la ligne 18 va générer une exception parce que le tableau tab n'a pas d'élément n° 100. L'exécution du programme donne les résultats suivants :

Cacher Sélectionnez
  1. tab[0]=0
  2. tab[1]=1
  3. tab[2]=2
  4. tab[3]=3
  5. 0
  6. 1
  7. 2
  8. 3
  9. L'erreur suivante s'est produite : System.IndexOutOfRangeException: L'index se trouve en dehors des limites du tableau.
  10.    à Chap1.P08.Main(String[] args) dans C:\data\travail\2007-2008\c# 2008\poly\Chap1\08\Program.cs:ligne 7
  11. finally ...
  • ligne 9 : l'exception [System.IndexOutOfRangeException] s'est produite
  • ligne 11 : la clause finally (lignes 23-25) du code a été exécutée, alors même que ligne 21, on avait une instruction return pour sortir de la méthode. On retiendra que la clause finally est toujours exécutée.

Voici un autre exemple où on gère l'exception provoquée par l'affectation d'une chaîne de caractères à un variable de type entier lorsque la chaîne ne représente pas un nombre entier :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap1 {
  4.     class P08 {
  5.         static void Main(string[] args) {
  6.
  7.             // exemple 2
  8.             // On demande le nom
  9.             Console.Write("Nom : ");
  10.             // lecture réponse
  11.             string nom = Console.ReadLine();
  12.             // on demande l'âge
  13.             int age = 0;
  14.             bool ageOK = false;
  15.             while (!ageOK) {
  16.                 // question
  17.                 Console.Write("âge : ");
  18.                 // lecture-vérification réponse
  19.                 try {
  20.                     age = int.Parse(Console.ReadLine());
  21.                     ageOK = age>=1;
  22.                 } catch {
  23.                 }//try-catch
  24.                 if (!ageOK) {
  25.                     Console.WriteLine("Age incorrect, recommencez...");
  26.                 }
  27.             }//while
  28.             // affichage final
  29.             Console.WriteLine("Vous vous appelez {0} et vous avez {1} an(s)",nom,age);
  30.         }
  31.     }
  32. }
  • lignes 15-27 : la boucle de saisie de l'âge d'une personne
  • ligne 20 : la ligne tapée au clavier est transformée en nombre entier par la méthode int.Parse . Cette méthode lance une exception si la conversion n'est pas possible. C'est pourquoi, l'opération a été placée dans un try / catch.
  • lignes 22-23 : si une exception est lancée, on va dans le catch où rien n'est fait. Ainsi, le booléen ageOK positionné à false , ligne 14, va-t-il rester à false .
  • ligne 21 : si on arrive à cette ligne, c'est que la conversion string -> int a réussi. On vérifie cependant que l'entier obtenu est bien supérieur ou égal à 1.
  • lignes 24-26 : un message d'erreur est émis si l'âge est incorrect.

Quelques résultats d'exécution :

Cacher Sélectionnez
  1. Nom : dupont
  2. âge : 23
  3. Vous vous appelez dupont et vous avez 23 an(s)
  4.
  5. Nom : durand
  6. âge : x
  7. Age incorrect, recommencez...
  8. âge : -4
  9. Age incorrect, recommencez...
  10. âge : 12
  11. Vous vous appelez durand et vous avez 12 an(s)

IV-G. Application exemple - version 1

On se propose d'écrire un programme permettant de calculer l'impôt d'un contribuable. On se place dans le cas simplifié d'un contribuable n'ayant que son seul salaire à déclarer (chiffres 2004 pour revenus 2003) :

  • on calcule le nombre de parts du salarié nbParts=nbEnfants/2 +1 s'il n'est pas marié, nbEnfants/2+2 s'il est marié, où nbEnfants est son nombre d'enfants.
  • s'il a au moins trois enfants, il a une demi part de plus
  • on calcule son revenu imposable R=0.72*S où S est son salaire annuel
  • on calcule son coefficient familial QF=R/nbParts
  • on calcule son impôt I. Considérons le tableau suivant :
4262 | 0 | 0
8382 | 0.0683 | 291.09
14753 | 0.1914 | 1322.92
23888 | 0.2826 | 2668.39
38868 | 0.3738 | 4846.98
47932 | 0.4262 | 6883.66
0 | 0.4809 | 9505.54

Chaque ligne a 3 champs. Pour calculer l'impôt I, on recherche la première ligne où QF<=champ1 . Par exemple, si QF=5000 on trouvera la ligne

83820.0683291.09

L'impôt I est alors égal à 0.0683*R - 291.09*nbParts. Si QF est tel que la relation QF<=champ1 n'est jamais vérifiée, alors ce sont les coefficients de la dernière ligne qui sont utilisés. Ici :

00.48099505.54

ce qui donne l'impôt I=0.4809*R - 9505.54*nbParts.

Le programme C# correspondant est le suivant :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap1 {
  4.     class Impots {
  5.         static void Main(string[] args) {
  6.             // tableaux de données nécessaires au calcul de l'impôt
  7.             decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
  8.             decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
  9.             decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };
  10.
  11.             // on récupère le statut marital
  12.             bool OK = false;
  13.             string reponse = null;
  14.             while (!OK) {
  15.                 Console.Write("Etes-vous marié(e) (O/N) ? ");
  16.                 reponse = Console.ReadLine().Trim().ToLower();
  17.                 if (reponse != "o" && reponse != "n")
  18.                     Console.Error.WriteLine("Réponse incorrecte. Recommencez");
  19.                 else OK = true;
  20.             }//while
  21.             bool marie = reponse == "o";
  22.
  23.             // nombre d'enfants
  24.             OK = false;
  25.             int nbEnfants = 0;
  26.             while (!OK) {
  27.                 Console.Write("Nombre d'enfants : ");
  28.                 try {
  29.                     nbEnfants = int.Parse(Console.ReadLine());
  30.                     OK = nbEnfants >= 0;
  31.                 } catch {
  32.                 }// try
  33.                 if (!OK) {
  34.                     Console.WriteLine("Réponse incorrecte. Recommencez");
  35.                 }
  36.             }// while
  37.
  38.             // salaire
  39.             OK = false;
  40.             int salaire = 0;
  41.             while (!OK) {
  42.                 Console.Write("Salaire annuel : ");
  43.                 try {
  44.                     salaire = int.Parse(Console.ReadLine());
  45.                     OK = salaire >= 0;
  46.                 } catch {
  47.                 }// try
  48.                 if (!OK) {
  49.                     Console.WriteLine("Réponse incorrecte. Recommencez");
  50.                 }
  51.             }// while
  52.
  53.             // calcul du nombre de parts
  54.             decimal nbParts;
  55.             if (marie) nbParts = (decimal)nbEnfants / 2 + 2;
  56.             else nbParts = (decimal)nbEnfants / 2 + 1;
  57.             if (nbEnfants >= 3) nbParts += 0.5M;
  58.
  59.             // revenu imposable
  60.             decimal revenu = 0.72M * salaire;
  61.
  62.             // quotient familial
  63.             decimal QF = revenu / nbParts;
  64.
  65.             // recherche de la tranche d'impots correspondant à QF
  66.             int i;
  67.             int nbTranches = limites.Length;
  68.             limites[nbTranches - 1] = QF;
  69.             i = 0;
  70.             while (QF > limites[i]) i++;
  71.             // l'impôt
  72.             int impots = (int)(coeffR[i] * revenu - coeffN[i] * nbParts);
  73.
  74.             // on affiche le résultat
  75.             Console.WriteLine("Impôt à payer : {0} euros", impots);
  76.         }
  77.     }
  78. }
  • lignes 7-9 : les valeurs numériques sont suffixées par M (Money) pour qu'elles soient de type decimal .
  • ligne 16 :
  • Console.ReadLine() rend la chaîne C1 tapée au clavier
  • C1.Trim() enlève les espaces de début et fin de C1 - rend une chaîne C2
  • C2.ToLower() rend la chaîne C3 qui est la chaîne C2 transformée en minuscules.
  • ligne 21 : le booléen marie reçoit la valeur true ou false de la relation reponse=="o"
  • ligne 29 : la chaîne tapée au clavier est transformée en type int . Si la transformation échoue, une exception est lancée.
  • ligne 30 : le booléen OK reçoit la valeur true ou false de la relation nbEnfants>=0
  • lignes 55-56 : on ne peut écrire simplement nbEnfants/2 . Si nbEnfants était égal à 3, on aurait 3/2, une division entière qui donnerait 1 et non 1.5. Aussi, écrit-on (decimal)nbEnfants pour rendre réel l'un des opérandes de la division et avoir ainsi une division entre réels.

Voici des exemples d'exécution :

Cacher Sélectionnez
  1. Etes-vous marié(e) (O/N) ? o
  2. Nombre d'enfants : 2
  3. Salaire annuel : 60000
  4. Impôt à payer : 4282 euros
  5.
  6. Etes-vous marié(e) (O/N) ? oui
  7. Réponse incorrecte. Recommencez
  8. Etes-vous marié(e) (O/N) ? o
  9. Nombre d'enfants : trois
  10. Réponse incorrecte. Recommencez
  11. Nombre d'enfants : 3
  12. Salaire annuel : 60000 euros
  13. Réponse incorrecte. Recommencez
  14. Salaire annuel : 60000
  15. Impôt à payer : 2959 euros

IV-H. Arguments du programme principal

La fonction principale Main peut admettre comme paramètre un tableau de chaînes : String[] (ou string[]) . Ce tableau contient les arguments de la ligne de commande utilisée pour lancer l'application. Ainsi si on lance le programme P avec la commande (Dos) suivante :

P arg0 arg1 … argn

et si la fonction Main est déclarée comme suit :

Cacher Sélectionnez
  1. public static void Main(string[] args)

on aura args[0]="arg0", args[1]="arg1" … Voici un exemple :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap1 {
  4.     class P10 {
  5.         static void Main(string[] args) {
  6.             // on liste les paramètres reçus
  7.             Console.WriteLine("Il y a  " + args.Length + " arguments");
  8.             for (int i = 0; i < args.Length; i++) {
  9.                 Console.Out.WriteLine("arguments[" + i + "]=" + args[i]);
  10.             }
  11.         }
  12.     }
  13. }

Pour passer des arguments au code exécuté, on procèdera comme suit :

Image non disponible
  • en [1] : clic droit sur le projet / Properties
  • en [2] : onglet [Debug]
  • en [3] : mettre les arguments

L'exécution donne les résultats suivants :

Cacher Sélectionnez
  1. Il y a  4 arguments
  2. arguments[0]=a0
  3. arguments[1]=a1
  4. arguments[2]=a2
  5. arguments[3]=a3

On notera que la signature

Cacher Sélectionnez
  1. public static void Main()

est valide si la fonction Main n'attend pas de paramètres.

IV-I. Les énumérations

Une énumération est un type de données dont le domaine de valeurs est un ensemble de constantes entières. Considérons un programme qui a à gérer des mentions à un examen. Il y en aurait cinq : Passable , AssezBien , Bien , TrèsBien , Excellent .

On pourrait alors définir une énumération pour ces cinq constantes :

Cacher Sélectionnez
  1.         enum Mentions { Passable, AssezBien, Bien, TrèsBien, Excellent };

De façon interne, ces cinq constantes sont codées par des entiers consécutifs commençant par 0 pour la première constante, 1 pour la suivante, etc… Une variable peut être déclarée comme prenant ces valeurs dans l'énumération :

Cacher Sélectionnez
  1.             // une variable qui prend ses valeurs dans l'énumération Mentions
  2. Mentions maMention = Mentions.Passable;

On peut comparer une variable aux différentes valeurs possibles de l'énumération :

Cacher Sélectionnez
  1.             if (maMention == Mentions.Passable) {
  2.                 Console.WriteLine("Peut mieux faire");
  3. }

On peut obtenir toutes les valeurs de l'énumération :

Cacher Sélectionnez
  1.             // liste des mentions sous forme de chaînes
  2.             foreach (Mentions m in Enum.GetValues(maMention.GetType())) {
  3.                 Console.WriteLine(m);
  4. }

De la même façon que le type simple int est équivalent à la structure System.Int32 , le type simple enum est équivalent à la structure System.Enum . Cette structure a une méthode statique GetValues qui permet d'obtenir toutes les valeurs d'un type énuméré que l'on passe en paramètre. Celui-ci doit être un objet de type Type qui est une classe d'informations sur le type d'une donnée. Le type d'une variable v est obtenu par v.GetType() . Le type d'un type T est obtenu par typeof(T) . Donc ici maMention.GetType() donne l'objet Type de l'énumération Mentions et Enum.GetValues(maMention.GetType()) la liste des valeurs de l'énumération Mentions .

Si on écrit maintenant

Cacher Sélectionnez
  1.             //liste des mentions sous forme d'entiers
  2.             foreach (int m in Enum.GetValues(typeof(Mentions))) {
  3.                 Console.WriteLine(m);
  4. }

Ligne 2, la variable de boucle est de type entier. On obtient alors la liste des valeurs de l'énumération sous forme d'entiers. L'objet de type System.Type correspondant au type de données Mentions est obtenu par typeof(Mentions) . On aurait pu écrire comme précédemment, maMention.GetType() .

Le programme suivant met en lumière ce qui vient d'être écrit :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap1 {
  4.     class P11 {
  5.         enum Mentions { Passable, AssezBien, Bien, TrèsBien, Excellent };
  6.         static void Main(string[] args) {
  7.             // une variable qui prend ses valeurs dans l'énumération Mentions
  8.             Mentions maMention = Mentions.Passable;
  9.             // affichage valeur variable
  10.             Console.WriteLine("mention=" + maMention);
  11.             // test avec valeur de l'énumération
  12.             if (maMention == Mentions.Passable) {
  13.                 Console.WriteLine("Peut mieux faire");
  14.             }
  15.             // liste des mentions sous forme de chaînes
  16.             foreach (Mentions m in Enum.GetValues(maMention.GetType())) {
  17.                 Console.WriteLine(m);
  18.             }
  19.             //liste des mentions sous forme d'entiers
  20.             foreach (int m in Enum.GetValues(typeof(Mentions))) {
  21.                 Console.WriteLine(m);
  22.             }
  23.         }
  24.     }
  25. }

Les résultats d'exécution sont les suivants :

Cacher Sélectionnez
  1. mention=Passable
  2. Peut mieux faire
  3. Passable
  4. AssezBien
  5. Bien
  6. TrèsBien
  7. Excellent
  8. 0
  9. 1
  10. 2
  11. 3
  12. 4

IV-J. Passage de paramètres à une fonction

Nous nous intéressons ici au mode de passage des paramètres d'une fonction. Considérons la fonction statique suivante :

Cacher Sélectionnez
  1.         private static void ChangeInt(int a) {
  2.             a = 30;
  3.             Console.WriteLine("Paramètre formel a=" + a);
  4. }

Dans la définition de la fonction, ligne1, a est appelé un paramètre formel. Il n'est là que pour les besoins de la définition de la fonction changeInt . Il aurait tout aussi bien pu s'appeler b . Considérons maintenant une utilisation de cette fonction :

Cacher Sélectionnez
  1.         public static void Main() {
  2.             int age = 20;
  3.             ChangeInt(age);
  4.             Console.WriteLine("Paramètre effectif age=" + age);
  5. }

Ici dans l'instruction de la ligne 3, ChangeInt(age) , age est le paramètre effectif qui va transmettre sa valeur au paramètre formel a . Nous nous intéressons à la façon dont un paramètre formel récupère la valeur d'un paramètre effectif.

IV-J-1. Passage par valeur

L'exemple suivant nous montre que les paramètres d'une fonction sont par défaut passés par valeur, c'est-à-dire que la valeur du paramètre effectif est recopiée dans le paramètre formel correspondant. On a deux entités distinctes. Si la fonction modifie le paramètre formel, le paramètre effectif n'est lui en rien modifié.

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap1 {
  4.     class P12 {
  5.         public static void Main() {
  6.             int age = 20;
  7.             ChangeInt(age);
  8.             Console.WriteLine("Paramètre effectif age=" + age);
  9.         }
  10.         private static void ChangeInt(int a) {
  11.             a = 30;
  12.             Console.WriteLine("Paramètre formel a=" + a);
  13.         }
  14.     }
  15. }

Les résultats obtenus sont les suivants :

Cacher Sélectionnez
  1. Paramètre formel a=30
  2. Paramètre effectif age=20

La valeur 20 du paramètre effectif age a été recopiée dans le paramètre formel a (ligne 10). Celui-ci a été ensuite modifié (ligne 11). Le paramètre effectif est lui resté inchangé. Ce mode de passage convient aux paramètres d'entrée d'une fonction.

IV-J-2. Passage par référence

Dans un passage par référence, le paramètre effectif et le paramètre formel sont une seule et même entité. Si la fonction modifie le paramètre formel, le paramètre effectif est lui aussi modifié. En C#, ils doivent être tous deux précédés du mot clé ref :

Voici un exemple :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap1 {
  4.     class P12 {
  5.         public static void Main() {
  6.             // exemple 2
  7.             int age2 = 20;
  8.             ChangeInt2(ref age2);
  9.             Console.WriteLine("Paramètre effectif age2=" + age2);
  10.         }
  11.         private static void ChangeInt2(ref int a2) {
  12.             a2 = 30;
  13.             Console.WriteLine("Paramètre formel a2=" + a2);
  14.         }
  15.     }
  16. }

et les résultats d'exécution :

Cacher Sélectionnez
  1. Paramètre formel a2=30
  2. Paramètre effectif age2=30

Le paramètre effectif a suivi la modification du paramètre formel. Ce mode de passage convient aux paramètres de sortie d'une fonction.

IV-J-3. Passage par référence avec le mot clé out

Considérons l'exemple précédent dans lequel la variable age2 ne serait pas initialisée avant l'appel à la fonction changeInt :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap1 {
  4.     class P12 {
  5.         public static void Main() {
  6.             // exemple 2
  7.             int age2;
  8.             ChangeInt2(ref age2);
  9.             Console.WriteLine("Paramètre effectif age2=" + age2);
  10.         }
  11.         private static void ChangeInt2(ref int a2) {
  12.             a2 = 30;
  13.             Console.WriteLine("Paramètre formel a2=" + a2);
  14.         }
  15.     }
  16. }

Lorsqu'on compile ce programme, on a une erreur :

Use of unassigned local variable 'age2'

On peut contourner l'obstacle en affectant une valeur initiale à age2 . On peut aussi remplacer le mot clé ref par le mot clé out. On exprime alors que la paramètre est uniquement un paramètre de sortie et n'a donc pas besoin de valeur initiale :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap1 {
  4.     class P12 {
  5.         public static void Main() {
  6.             // exemple 3
  7.             int age3;
  8.             ChangeInt3(out age3);
  9.             Console.WriteLine("Paramètre effectif age3=" + age3);
  10.         }
  11.         private static void ChangeInt3(out int a3) {
  12.             a3 = 30;
  13.             Console.WriteLine("Paramètre formel a3=" + a3);
  14.         }
  15.     }
  16. }

Les résultats de l'exécution sont les suivants :

Cacher Sélectionnez
  1. Paramètre formel a3=30
  2. Paramètre effectif age3=30


CSharp tutoriel 2008

Apprentissage du langage C#

  • V. Classes, Stuctures, Interfaces
    • V-A. L' objet par l'exemple
      • V-A-1. Généralités
      • V-A-2. Création du projet C#
      • V-A-3. Définition de la classe Personne
      • V-A-4. La méthode Initialise
      • V-A-5. L'opérateur new
      • V-A-6. Le mot clé this
      • V-A-7. Un programme de test
      • V-A-8. Une autre méthode Initialise
      • V-A-9. Constructeurs de la classe Personne
      • V-A-10. Les références d'objets
      • V-A-11. Passage de paramètres de type référence d'objet
      • V-A-12. Les objets temporaires
      • V-A-13. Méthodes de lecture et d'écriture des attributs privés
      • V-A-14. Les propriétés
      • V-A-15. Les méthodes et attributs de classe
      • V-A-16. Un tableau de personnes
    • V-B. L'héritage par l'exemple
      • V-B-1. Généralités
      • V-B-2. Construction d'un objet Enseignant
      • V-B-3. Redéfinition d'une méthode ou d'une propriété
      • V-B-4. Le polymorphisme
      • V-B-5. Redéfinition et polymorphisme
    • V-C. Redéfinir la signification d'un opérateur pour une classe
      • V-C-1. Introduction
      • V-C-2. Un exemple
    • V-D. Définir un indexeur pour une classe
    • V-E. Les structures
    • V-F. Les interfaces
    • V-G. Les classes abstraites
    • V-H. Les classes, interfaces, méthodes génériques
    • V-I. Les espaces de noms
    • V-J. Application exemple - version 2

V. Classes, Stuctures, Interfaces

V-A. L' objet par l'exemple

V-A-1. Généralités

Nous abordons maintenant, par l'exemple, la programmation objet. Un objet est une entité qui contient des données qui définissent son état (on les appelle des champs, attributs…) et des fonctions (on les appelle des méthodes). Un objet est créé selon un modèle qu'on appelle une classe :

Cacher Sélectionnez
  1. public class C1{
  2.     Type1 p1;        // champ p1
  3.     Type2 p2;        // champ p2
  4.     …
  5.     Type3 m3(…){        // méthode m3
  6.         …
  7.     }
  8.     Type4 m4(…){        // méthode m4
  9.         …
  10.     }
  11.     …
  12. }

A partir de la classe C1 précédente, on peut créer de nombreux objets O1, O2,… Tous auront les champs p1, p2,… et les méthodes m3, m4… Mais ils auront des valeurs différentes pour leurs champs pi ayant ainsi chacun un état qui leur est propre. Si o 1 est un objet de type C1 , o 1.p1 désigne la propriété p1 de o 1 et o 1.m1 la méthode m1 de O1 .

Considérons un premier modèle d'objet : la classe Personne .

V-A-2. Création du projet C#

Dans les exemples précédents, nous n'avions dans un projet qu'un unique fichier source : Program.cs . A partir de maintenant, nous pourrons avoir plusieurs fichiers source dans un même projet. Nous montrons comment procéder.

Image non disponible

En [1], créez un nouveau projet. En [2], choisissez une Application Console. En [3], laissez la valeur par défaut. En [4], validez. En [5], le projet qui a été généré. Le contenu de Program.cs est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5.
  6. namespace ConsoleApplication1 {
  7.     class Program {
  8.         static void Main(string[] args) {
  9.         }
  10.     }
  11. }

Sauvegardons le projet créé :

Image non disponible

En [1], l'option de sauvegarde. En [2], désignez le dossier où sauvegarder le projet. En [3], donnez un nom au projet. En [5], indiquez que vous voulez créer une solution. Une solution est un ensemble de projets. En [4], donnez le nom de la solution. En [6], validez la sauvegarde.

Image non disponible

En [1], le projet sauvegardé. En [2], ajoutez un nouvel élément au projet.

Image non disponible

En [1], indiquez que vous voulez ajouter une classe. En [2], le nom de la classe. En [3], validez les informations. En [4], le projet [01] a un nouveau fichier source Personne.cs :

Cacher Sélectionnez
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5.
  6. namespace ConsoleApplication1 {
  7.     class Personne {
  8.     }
  9. }

On modifie l'espace de noms de chacun des fichiers source en Chap2 et on supprime l'importation des espaces de noms inutiles :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Personne {
  5.     }
  6. }
Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program {
  5.         static void Main(string[] args) {
  6.         }
  7.     }
  8. }

V-A-3. Définition de la classe Personne

La définition de la classe Personne dans le fichier source [ Personne.cs ] sera la suivante :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     public class Personne {
  5.         // attributs
  6.         private string prenom;
  7.         private string nom;
  8.         private int age;
  9.
  10.         // méthode
  11.         public void Initialise(string P, string N, int age) {
  12.             this.prenom = P;
  13.             this.nom = N;
  14.             this.age = age;
  15.         }
  16.
  17.         // méthode
  18.         public void Identifie() {
  19.             Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
  20.         }
  21.     }
  22.
  23. }

Nous avons ici la définition d'une classe, donc d'un type de données. Lorsqu'on va créer des variables de ce type, on les appellera des objets ou des instances de classe. Une classe est donc un moule à partir duquel sont construits des objets.

Les membres ou champs d'une classe peuvent être des données (attributs), des méthodes (fonctions), des propriétés. Les propriétés sont des méthodes particulières servant à connaître ou fixer la valeur d'attributs de l'objet. Ces champs peuvent être accompagnés de l'un des trois mots clés suivants :

privé | Un champ privé (private) n'est accessible que par les seules méthodes internes de la classe
public | Un champ public (public) est accessible par toute méthode définie ou non au sein de la classe
protégé | Un champ protégé (protected) n'est accessible que par les seules méthodes internes de la classe ou d'un objet dérivé (voir ultérieurement le concept d'héritage).

En général, les données d'une classe sont déclarées privées alors que ses méthodes et propriétés sont déclarées publiques. Cela signifie que l'utilisateur d'un objet (le programmeur)

  • n'aura pas accès directement aux données privées de l'objet
  • pourra faire appel aux méthodes publiques de l'objet et notamment à celles qui donneront accès à ses données privées.

La syntaxe de déclaration d'une classe C est la suivante :

Cacher Sélectionnez
  1. public class C{
  2.     private  donnée ou méthode ou propriété privée;
  3.     public  donnée ou méthode ou propriété publique;
  4.     protected  donnée ou méthode ou propriété protégée;
  5. }

L'ordre de déclaration des attributs private, protected et public est quelconque.

V-A-4. La méthode Initialise

Revenons à notre classe Personne déclarée comme :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     public class Personne {
  5.         // attributs
  6.         private string prenom;
  7.         private string nom;
  8.         private int age;
  9.
  10.         // méthode
  11.         public void Initialise(string p, string n, int age) {
  12.             this.prenom = p;
  13.             this.nom = n;
  14.             this.age = age;
  15.         }
  16.
  17.         // méthode
  18.         public void Identifie() {
  19.             Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
  20.         }
  21.     }
  22.
  23. }

Quel est le rôle de la méthode I nitialise ? Parce que nom , prenom et age sont des données privées de la classe P ersonne , les instructions :

Cacher Sélectionnez
  1. Personne p1;
  2. p1.prenom="Jean";
  3. p1.nom="Dupont";
  4. p1.age=30;

sont illégales. Il nous faut initialiser un objet de type Personne via une méthode publique. C'est le rôle de la méthode Initialise . On écrira :

Cacher Sélectionnez
  1. Personne p1;
  2. p1.Initialise("Jean","Dupont",30);

L'écriture p1.Initialise est légale car Initialise est d'accès public.

V-A-5. L'opérateur new

La séquence d'instructions

Cacher Sélectionnez
  1. Personne p1;
  2. p1.Initialise("Jean","Dupont",30);

est incorrecte. L'instruction

Cacher Sélectionnez
  1.     Personne p1;

déclare p1 comme une référence à un objet de type Personne . Cet objet n'existe pas encore et donc p1 n'est pas initialisé. C'est comme si on écrivait :

Cacher Sélectionnez
  1. Personne p1=null;

où on indique explicitement avec le mot clé null que la variable p1 ne référence encore aucun objet. Lorsqu'on écrit ensuite

Cacher Sélectionnez
  1. p1.Initialise("Jean","Dupont",30);

on fait appel à la méthode Initialise de l'objet référencé par p1 . Or cet objet n'existe pas encore et le compilateur signalera l'erreur. Pour que p1 référence un objet, il faut écrire :

Cacher Sélectionnez
  1. Personne p1=new Personne();

Cela a pour effet de créer un objet de type Personne non encore initialisé : les attributs nom et prenom qui sont des références d'objets de type String auront la valeur null , et age la valeur 0 . Il y a donc une initialisation par défaut. Maintenant que p1 référence un objet, l'instruction d'initialisation de cet objet

Cacher Sélectionnez
  1. p1.Initialise("Jean","Dupont",30);

est valide.

V-A-6. Le mot clé this

Regardons le code de la méthode initialise :

Cacher Sélectionnez
  1.         public void Initialise(string p, string n, int age) {
  2.             this.prenom = p;
  3.             this.nom = n;
  4.             this.age = age;
  5. }

L'instruction this.prenom=p signifie que l'attribut prenom de l'objet courant ( this ) reçoit la valeur p . Le mot clé this désigne l'objet courant : celui dans lequel se trouve la méthode exécutée. Comment le connaît-on ? Regardons comment se fait l'initialisation de l'objet référencé par p1 dans le programme appelant :

Cacher Sélectionnez
  1. p1.Initialise("Jean","Dupont",30);

C'est la méthode Initialise de l'objet p1 qui est appelée. Lorsque dans cette méthode, on référence l'objet this , on référence en fait l'objet p1 . La méthode Initialise aurait aussi pu être écrite comme suit :

Cacher Sélectionnez
  1.         public void Initialise(string p, string n, int age) {
  2.             prenom = p;
  3.             nom = n;
  4.             this.age = age;
  5. }

Lorsqu'une méthode d'un objet référence un attribut A de cet objet, l'écriture this.A est implicite. On doit l'utiliser explicitement lorsqu'il y a conflit d'identificateurs. C'est le cas de l'instruction :

Cacher Sélectionnez
  1. this.age=age;

où age désigne un attribut de l'objet courant ainsi que le paramètre age reçu par la méthode. Il faut alors lever l'ambiguïté en désignant l'attribut age par this.age .

V-A-7. Un programme de test

Voici un court programme de test. Celui-ci est écrit dans le fichier source [ Program.cs ] :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class P01 {
  5.         static void Main() {
  6.             Personne p1 = new Personne();
  7.             p1.Initialise("Jean", "Dupont", 30);
  8.             p1.Identifie();
  9.         }
  10.     }
  11. }

Avant d'exécuter le projet [01], il peut être nécessaire de préciser le fichier source à exécuter :

Image non disponible

Dans les propriétés du projet [01], on indique en [1] la classe à exécuter.

Les résultats obtenus à l'exécution sont les suivants :

Cacher Sélectionnez
  1. [Jean, Dupont, 30]

V-A-8. Une autre méthode Initialise

Considérons toujours la classe Personne et rajoutons-lui la méthode suivante :

Cacher Sélectionnez
  1.         public void Initialise(Personne p) {
  2.             prenom = p.prenom;
  3.             nom = p.nom;
  4.             age = p.age;
  5. }

On a maintenant deux méthodes portant le nom Initialise : c'est légal tant qu'elles admettent des paramètres différents. C'est le cas ici. Le paramètre est maintenant une référence p à une personne. Les attributs de la personne p sont alors affectés à l'objet courant ( this ). On remarquera que la méthode Initialise a un accès direct aux attributs de l'objet p bien que ceux-ci soient de type private . C'est toujours vrai : un objet o 1 d'une classe C a toujours accès aux attributs des objets de la même classe C .

Voici un test de la nouvelle classe P ersonne :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program {
  5.         static void Main() {
  6.             Personne p1 = new Personne();
  7.             p1.Initialise("Jean", "Dupont", 30);
  8.             p1.Identifie();
  9.             Personne p2 = new Personne();
  10.             p2.Initialise(p1);
  11.             p2.Identifie();
  12.         }
  13.     }
  14. }

et ses résultats :

Cacher Sélectionnez
  1. [Jean, Dupont, 30]
  2. [Jean, Dupont, 30]

V-A-9. Constructeurs de la classe Personne

Un constructeur est une méthode qui porte le nom de la classe et qui est appelée lors de la création de l'objet. On s'en sert généralement pour l'initialiser. C'est une méthode qui peut accepter des arguments mais qui ne rend aucun résultat. Son prototype ou sa définition ne sont précédés d'aucun type (pas même void ).

Si une classe C a un constructeur acceptant n arguments argi , la déclaration et l'initialisation d'un objet de cette classe pourra se faire sous la forme :

C objet =new C( arg1,arg2… argn );

ou

C objet;

…

objet=new C( arg1,arg2… argn );

Lorsqu'une classe C a un ou plusieurs constructeurs, l'un de ces constructeurs doit être obligatoirement utilisé pour créer un objet de cette classe. Si une classe C n'a aucun constructeur, elle en a un par défaut qui est le constructeur sans paramètres : public C() . Les attributs de l'objet sont alors initialisés avec des valeurs par défaut. C'est ce qui s'est passé lorsque dans les programmes précédents, où on avait écrit :

Cacher Sélectionnez
  1.     Personne p1;
  2.     p1=new Personne();

Créons deux constructeurs à notre classe Personne :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     public class Personne {
  5.         // attributs
  6.         private string prenom;
  7.         private string nom;
  8.         private int age;
  9.
  10.         // constructeurs
  11.         public Personne(String p, String n, int age) {
  12.             Initialise(p, n, age);
  13.         }
  14.         public Personne(Personne P) {
  15.             Initialise(P);
  16.         }
  17.
  18.         // méthode
  19.         public void Initialise(string p, string n, int age) {
  20. ...
  21.         }
  22.
  23.         public void Initialise(Personne p) {
  24. ...
  25.         }
  26.
  27.         // méthode
  28.         public void Identifie() {
  29.             Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
  30.         }
  31.     }
  32.
  33. }

Nos deux constructeurs se contentent de faire appel aux méthodes Initialise étudiées précédemment. On rappelle que lorsque dans un constructeur, on trouve la notation Initialise(p) par exemple, le compilateur traduit par this.Initialise(p) . Dans le constructeur, la méthode Initialise est donc appelée pour travailler sur l'objet référencé par this , c'est-à-dire l'objet courant, celui qui est en cours de construction.

Voici un court programme de test :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program {
  5.         static void Main() {
  6.             Personne p1 = new Personne("Jean", "Dupont", 30);
  7.             p1.Identifie();
  8.             Personne p2 = new Personne(p1);
  9.             p2.Identifie();
  10.         }
  11.     }
  12. }

et les résultats obtenus :

Cacher Sélectionnez
  1. [Jean, Dupont, 30]
  2. [Jean, Dupont, 30]

V-A-10. Les références d'objets

Nous utilisons toujours la même classe Personne . Le programme de test devient le suivant :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program2 {
  5.         static void Main() {
  6.             // p1
  7.             Personne p1 = new Personne("Jean", "Dupont", 30);
  8.             Console.Write("p1="); p1.Identifie();
  9.             // p2 référence le même objet que p1
  10.             Personne p2 = p1;
  11.             Console.Write("p2="); p2.Identifie();
  12.             // p3 référence un objet qui sera une copie de l'objet référencé par p1
  13.             Personne p3 = new Personne(p1);
  14.             Console.Write("p3="); p3.Identifie();
  15.             // on change l'état de l'objet référencé par p1
  16.             p1.Initialise("Micheline", "Benoît", 67);
  17.             Console.Write("p1="); p1.Identifie();
  18.             // comme p2=p1, l'objet référencé par p2 a du changer d'état
  19.             Console.Write("p2="); p2.Identifie();
  20.             // comme p3 ne référence pas le même objet que p1, l'objet référencé par p3 n'a pas du changer
  21.             Console.Write("p3="); p3.Identifie();
  22.         }
  23.     }
  24. }

Les résultats obtenus sont les suivants :

Cacher Sélectionnez
  1. p1=[Jean, Dupont, 30]
  2. p2=[Jean, Dupont, 30]
  3. p3=[Jean, Dupont, 30]
  4. p1=[Micheline, Benoît, 67]
  5. p2=[Micheline, Benoît, 67]
  6. p3=[Jean, Dupont, 30]

Lorsqu'on déclare la variable p1 par

Cacher Sélectionnez
  1. Personne p1=new Personne("Jean","Dupont",30);

p1 référence l'objet Personne("Jean","Dupont",30) mais n'est pas l'objet lui-même. En C, on dirait que c'est un pointeur, c.a.d. l'adresse de l'objet créé. Si on écrit ensuite :

Cacher Sélectionnez
  1.     p1=null;

Ce n'est pas l'objet Personne("Jean","Dupont",30) qui est modifié, c'est la référence p1 qui change de valeur. L'objet Personne("Jean","Dupont",30) sera "perdu" s'il n'est référencé par aucune autre variable.

Lorsqu'on écrit :

Cacher Sélectionnez
  1. Personne p2=p1;

on initialise le pointeur p2 : il "pointe" sur le même objet (il désigne le même objet) que le pointeur p1 . Ainsi si on modifie l'objet "pointé" (ou référencé) par p1 , on modifie aussi celui référencé par p2 .

Lorsqu'on écrit :

Cacher Sélectionnez
  1. Personne p3=new Personne(p1);

il y a création d'un nouvel objet Personne . Ce nouvel objet sera référencé par p3 . Si on modifie l'objet "pointé" (ou référencé) par p1 , on ne modifie en rien celui référencé par p3 . C'est ce que montrent les résultats obtenus.

V-A-11. Passage de paramètres de type référence d'objet

Dans le chapitre précédent, nous avons étudié les modes de passage des paramètres d'une fonction lorsque ceux-ci représentaient un type C# simple représenté par une structure .NET. Voyons ce qui se passe lorsque la paramètre est une référence d'objet :

Cacher Sélectionnez
  1. using System;
  2. using System.Text;
  3.
  4. namespace Chap1 {
  5.     class P12 {
  6.         public static void Main() {
  7.             // exemple 4
  8.             StringBuilder sb0 = new StringBuilder("essai0"), sb1 = new StringBuilder("essai1"), sb2 = new StringBuilder("essai2"), sb3;
  9.             Console.WriteLine("Dans fonction appelante avant appel : sb0={0}, sb1={1}, sb2={2}", sb0,sb1, sb2);
  10.             ChangeStringBuilder(sb0, sb1, ref sb2, out sb3);
  11.             Console.WriteLine("Dans fonction appelante après appel : sb0={0}, sb1={1}, sb2={2}, sb3={3}", sb0, sb1, sb2, sb3);
  12.
  13.         }
  14.
  15.         private static void ChangeStringBuilder(StringBuilder sbf0, StringBuilder sbf1, ref StringBuilder sbf2, out StringBuilder sbf3) {
  16.             Console.WriteLine("Début fonction appelée : sbf0={0}, sbf1={1}, sbf2={2}", sbf0,sbf1, sbf2);
  17.             sbf0.Append("*****");
  18.             sbf1 = new StringBuilder("essai1*****");
  19.             sbf2 = new StringBuilder("essai2*****");
  20.             sbf3 = new StringBuilder("essai3*****");
  21.             Console.WriteLine("Fin fonction appelée : sbf0={0}, sbf1={1}, sbf2={2}, sbf3={3}", sbf0, sbf1, sbf2, sbf3);
  22.         }
  23.     }
  24. }
  • ligne 8 : définit 3 objets de type StringBuilder . Un objet StringBuilder est proche d'un objet string. Lorsqu'on manipule un objet string , on obtient en retour un nouvel objet string . Ainsi dans la séquence de code :
Cacher Sélectionnez
  1. string s="une chaîne";
  2. s=s.ToUpperCase();

La ligne 1 crée un objet string en mémoire et s est son adresse. Ligne 2, s.ToUpperCase() crée un autre objet string en mémoire. Ainsi entre les lignes 1 et 2, s a changé de valeur (il pointe sur le nouvel objet). La classe StringBuilder elle, permet de transformer une chaîne sans qu'un second objet soit créé. C'est l'exemple donné plus haut :

  • ligne 8 : 4 références [sb0, sb1, sb2, sb3] à des objets de type StringBuilder
  • ligne 10 : sont passées à la méthode ChangeStringBuilder avec des modes différents : sb0 , sb1 avec le mode par défaut, sb2 avec le mot clé ref, sb3 avec le mot clé out.
  • lignes 15-22 : une méthode qui a les paramètres formels [sbf0, sbf1, sbf2, sbf3]. Les relations entre paramètres formels formels sbfi et effectifs sbi sont les suivantes :
  • sbf0 et sb0 sont, au démarrage de la méthode, deux références distinctes qui pointent sur le même objet (passage par valeur des adresses)
  • idem pour sbf1 et sb1
  • sbf2 et sb2 sont, au démarrage de la méthode, une même référence sur le même objet (mot clé ref )
  • sbf3 et sb3 sont, après exécution de la méthode, une même référence sur le même objet (mot clé out )

Les résultats obtenus sont les suivants :

Cacher Sélectionnez
  1. Dans fonction appelante avant appel : sb0=essai0, sb1=essai1, sb2=essai2
  2. Début fonction appelée : sbf0=essai0, sbf1=essai1, sbf2=essai2
  3. Fin fonction appelée : sbf0=essai0*****, sbf1=essai1*****, sbf2=essai2*****, sbf3=essai3*****
  4. Dans fonction appelante après appel : sb0=essai0*****, sb1=essai1, sb2=essai2*****, sb3=essai3*****

Explications :

  • sb0 et sbf0 sont deux références distinctes sur le même objet. Celui-ci a été modifié via sbf0 - ligne 3. Cette modification peut être vue via sb0 - ligne 4.
  • sb1 et sbf1 sont deux références distinctes sur le même objet. sbf1 voit sa valeur modifiée dans la méthode et pointe désormais sur un nouvel objet - ligne 3. Cela ne change en rien la valeur de sb1 qui continue à pointer sur le même objet - ligne 4.
  • sb2 et sbf2 sont une même référence sur le même objet. sbf2 voit sa valeur modifiée dans la méthode et pointe désormais sur un nouvel objet - ligne 3. Comme sbf2 et sb2 sont une seule et même entité, la valeur de sb2 a été également modifiée et sb2 pointe sur le même objet que sbf2 - lignes 3 et 4.
  • avant appel de la méthode, sb3 n'avait pas de valeur. Après la méthode, sb3 reçoit la valeur de sbf3 . On a donc deux références sur le même objet - lignes 3 et 4

V-A-12. Les objets temporaires

Dans une expression, on peut faire appel explicitement au constructeur d'un objet : celui-ci est construit, mais nous n'y avons pas accès (pour le modifier par exemple). Cet objet temporaire est construit pour les besoins d'évaluation de l'expression puis abandonné. L'espace mémoire qu'il occupait sera automatiquement récupéré ultérieurement par un programme appelé "ramasse-miettes" dont le rôle est de récupérer l'espace mémoire occupé par des objets qui ne sont plus référencés par des données du programme.

Considérons le nouveau programme de test suivant :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program {
  5.         static void Main() {
  6.             new Personne(new Personne("Jean", "Dupont", 30)).Identifie();
  7.         }
  8.     }
  9. }

et modifions les constructeurs de la classe Personne afin qu'ils affichent un message :

Cacher Sélectionnez
  1.         // constructeurs
  2.         public Personne(String p, String n, int age) {
  3.             Console.WriteLine("Constructeur Personne(string, string, int)");
  4.             Initialise(p, n, age);
  5.         }
  6.         public Personne(Personne P) {
  7.             Console.Out.WriteLine("Constructeur Personne(Personne)");
  8.             Initialise(P);
  9. }

Nous obtenons les résultats suivants :

Cacher Sélectionnez
  1. Constructeur Personne(string, string, int)
  2. Constructeur Personne(Personne)
  3. [Jean, Dupont, 30]

montrant la construction successive des deux objets temporaires.

V-A-13. Méthodes de lecture et d'écriture des attributs privés

Nous rajoutons à la classe Personne les méthodes nécessaires pour lire ou modifier l'état des attributs des objets :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     public class Personne {
  5.         // attributs
  6.         private string prenom;
  7.         private string nom;
  8.         private int age;
  9.
  10.         // constructeurs
  11.         public Personne(String p, String n, int age) {
  12.             Console.WriteLine("Constructeur Personne(string, string, int)");
  13.             Initialise(p, n, age);
  14.         }
  15.         public Personne(Personne p) {
  16.             Console.Out.WriteLine("Constructeur Personne(Personne)");
  17.             Initialise(p);
  18.         }
  19.
  20.         // méthode
  21.         public void Initialise(string p, string n, int age) {
  22.             this.prenom = p;
  23.             this.nom = n;
  24.             this.age = age;
  25.         }
  26.
  27.         public void Initialise(Personne p) {
  28.             prenom = p.prenom;
  29.             nom = p.nom;
  30.             age = p.age;
  31.         }
  32.
  33.         // accesseurs
  34.         public String GetPrenom() {
  35.             return prenom;
  36.         }
  37.         public String GetNom() {
  38.             return nom;
  39.         }
  40.         public int GetAge() {
  41.             return age;
  42.         }
  43.
  44.         //modifieurs
  45.         public void SetPrenom(String P) {
  46.             this.prenom = P;
  47.         }
  48.         public void SetNom(String N) {
  49.             this.nom = N;
  50.         }
  51.         public void SetAge(int age) {
  52.             this.age = age;
  53.         }
  54.
  55.         // méthode
  56.         public void Identifie() {
  57.             Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
  58.         }
  59.     }
  60.
  61. }

Nous testons la nouvelle classe avec le programme suivant :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program {
  5.         static void Main(string[] args) {
  6.             Personne p = new Personne("Jean", "Michelin", 34);
  7.             Console.Out.WriteLine("p=(" + p.GetPrenom() + "," + p.GetNom() + "," + p.GetAge() + ")");
  8.             p.SetAge(56);
  9.             Console.Out.WriteLine("p=(" + p.GetPrenom() + "," + p.GetNom() + "," + p.GetAge() + ")");
  10.         }
  11.     }
  12. }

et nous obtenons les résultats :

Cacher Sélectionnez
  1. Constructeur Personne(string, string, int)
  2. p=(Jean,Michelin,34)
  3. p=(Jean,Michelin,56)

V-A-14. Les propriétés

Il existe une autre façon d'avoir accès aux attributs d'une classe, c'est de créer des propriétés. Celles-ci nous permettent de manipuler des attributs privés comme s'ils étaient publics.

Considérons la classe P ersonne suivante où les accesseurs et modifieurs précédents ont été remplacés par des propriétés en lecture et écriture :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     public class Personne {
  5.         // attributs
  6.         private string prenom;
  7.         private string nom;
  8.         private int age;
  9.
  10.         // constructeurs
  11.         public Personne(String p, String n, int age) {
  12.             Initialise(p, n, age);
  13.         }
  14.         public Personne(Personne p) {
  15.             Initialise(p);
  16.         }
  17.
  18.         // méthode
  19.         public void Initialise(string p, string n, int age) {
  20.             this.prenom = p;
  21.             this.nom = n;
  22.             this.age = age;
  23.         }
  24.
  25.         public void Initialise(Personne p) {
  26.             prenom = p.prenom;
  27.             nom = p.nom;
  28.             age = p.age;
  29.         }
  30.
  31.         // propriétés
  32.         public string Prenom {
  33.             get { return prenom; }
  34.             set {
  35.                 // prénom valide ?
  36.                 if (value == null || value.Trim().Length == 0) {
  37.                     throw new Exception("prénom (" + value + ") invalide");
  38.                 } else {
  39.                     prenom = value;
  40.                 }
  41.             }//if
  42.         }//prenom
  43.
  44.         public string Nom {
  45.             get { return nom; }
  46.             set {
  47.                 // nom valide ?
  48.                 if (value == null || value.Trim().Length == 0) {
  49.                     throw new Exception("nom (" + value + ") invalide");
  50.                 } else { nom = value; }
  51.             }//if
  52.         }//nom
  53.
  54.
  55.         public int Age {
  56.             get { return age; }
  57.             set {
  58.                 // age valide ?
  59.                 if (value >= 0) {
  60.                     age = value;
  61.                 } else
  62.                     throw new Exception("âge (" + value + ") invalide");
  63.             }//if
  64.         }//age
  65.
  66.         // méthode
  67.         public void Identifie() {
  68.             Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
  69.         }
  70.     }
  71.
  72. }

Une propriété permet de lire (get) ou de fixer (set) la valeur d'un attribut. Une propriété est déclarée comme suit :

Cacher Sélectionnez
public Type Propriété{
    get {...}
    set {...}
}

où Type doit être le type de l'attribut géré par la propriété. Elle peut avoir deux méthodes appelées get et set. La méthode get est habituellement chargée de rendre la valeur de l'attribut qu'elle gère (elle pourrait rendre autre chose, rien ne l'empêche). La méthode set reçoit un paramètre appelé value qu'elle affecte normalement à l'attribut qu'elle gère. Elle peut en profiter pour faire des vérifications sur la validité de la valeur reçue et éventuellement lancer un exception si la valeur se révèle invalide. C'est ce qui est fait ici.

Comment ces méthodes get et set sont-elles appelées ? Considérons le programme de test suivant :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program {
  5.         static void Main(string[] args) {
  6.             Personne p = new Personne("Jean", "Michelin", 34);
  7.             Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");
  8.             p.Age = 56;
  9.             Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");
  10.             try {
  11.                 p.Age = -4;
  12.             } catch (Exception ex) {
  13.                 Console.Error.WriteLine(ex.Message);
  14.             }//try-catch
  15.         }
  16.     }
  17. }

Dans l'instruction

Cacher Sélectionnez
  1.     Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");

on cherche à avoir les valeurs des propriétés P renom , N om et A ge de la personne p. C'est la méthode get de ces propriétés qui est alors appelée et qui rend la valeur de l'attribut qu'elles gèrent.

Dans l'instruction

Cacher Sélectionnez
  1.         p.Age=56;

on veut fixer la valeur de la propriété A ge . C'est alors la méthode set de cette propriété qui est alors appelée. Elle recevra 56 dans son paramètre value.

Une propriété P d'une classe C qui ne définirait que la méthode get est dite en lecture seule. Si c est un objet de classe C, l'opération c.P=valeur sera alors refusée par le compilateur.

L'exécution du programme de test précédent donne les résultats suivants :

Cacher Sélectionnez
  1. p=(Jean,Michelin,34)
  2. p=(Jean,Michelin,56)
  3. âge (-4) invalide

Les propriétés nous permettent donc de manipuler des attributs privés comme s'ils étaient publics. Une autre caractéristique des propriétés est qu'elles peuvent être utilisées conjointement avec un constructeur selon la syntaxe suivante :

Cacher Sélectionnez
  1. Classe objet=new Classe (...) {Propriété1=val1, Propriété2=val2, ...}

Cette syntaxe est équivalente au code suivant :

Cacher Sélectionnez
  1. Classe objet=new Classe(...);
  2. objet.Propriété1=val1;
  3. objet.Propriété2=val2;
  4. ...

L'ordre des propriétés n'importe pas. Voici un exemple.

La classe Personne se voit ajouter un nouveau constructeur sans paramètres :

Cacher Sélectionnez
  1.         public Personne() {
  2. }

Le constructeur n'initialise pas les membres de l'objet. C'est ce qu'on appelle le constructeur par défaut. C'est lui qui est utilisé lorsque la classe ne définit aucun constructeur.

Le code suivant crée et initialise (ligne 6) une nouvelle Personne avec la syntaxe présentée précédemment :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program {
  5.         static void Main(string[] args) {
  6.             Personne p2 = new Personne { Age = 7, Prenom = "Arthur", Nom = "Martin" };
  7.             Console.WriteLine("p2=({0},{1},{2})", p2.Prenom, p2.Nom, p2.Age);
  8.         }
  9.     }
  10. }

Ligne 6 ci-dessus, c'est le constructeur sans paramètres Personne() qui est utilisé. Dans ce cas particulier, on aurait pu aussi écrire

Cacher Sélectionnez
  1.             Personne p2 = new Personne() { Age = 7, Prenom = "Arthur", Nom = "Martin" };

mais les parenthèses du constructeur Personne () sans paramètres ne sont pas obligatoires dans cette syntaxe.

Les résultats de l'exécution sont les suivants :

Cacher Sélectionnez
  1. p2=(Arthur,Martin,7)

Dans beaucoup de cas, les méthodes get et set d'une propriété se contentent de lire et écrire un champ privé sans autre traitement. On peut alors, dans ce scénario, utiliser une propriété automatique déclarée comme suit :

public Type Propriété{ get ; set ; }

Le champ privé associé à la propriété n'est pas déclaré. Il est automatiquement généré par le compilateur. On y accède que via sa propriété. Ainsi, au lieu d'écrire :

Cacher Sélectionnez
  1.     private string prenom;
  2. ...
  3.     // propriété associée
  4.         public string Prenom {
  5.             get { return prenom; }
  6.             set {
  7.                 // prénom valide ?
  8.                 if (value == null || value.Trim().Length == 0) {
  9.                     throw new Exception("prénom (" + value + ") invalide");
  10.                 } else {
  11.                     prenom = value;
  12.                 }
  13.             }//if
  14.         }//prenom

on pourra écrire :

Cacher Sélectionnez
  1. public string Prenom {get; set;}

sans déclarer le champ privé prenom . La différence entre les deux propriétés précédentes est que la première vérifie la validité du prénom dans le set , alors que la deuxième ne fait aucune vérification.

Utiliser la propriété automatique Prenom revient à déclarer un champ Prenom public :

Cacher Sélectionnez
  1. public string Prenom;

On peut se demander s'il y a une différence entre les deux déclarations. Déclarer public un champ d'une classe est déconseillé. Cela rompt avec le concept d'encapsulation de l'état d'un objet, état qui doit être privé et exposé par des méthodes publiques.

Si la propriété automatique est déclarée virtuelle, elle peut alors être redéfinie dans une classe fille :

Cacher Sélectionnez
  1.     class Class1 {
  2.         public virtual string Prop { get; set; }
  3. }
Cacher Sélectionnez
  1.     class Class2 : Class1 {
  2.         public override string Prop { get { return base.Prop; } set {... } }
  3. }

Ligne 2 ci-dessus, la classe fille Class2 peut mettre dans le set, du code vérifiant la validité de la valeur affectée à la propriété automatique base.Prop de la classe mère Class1 .

V-A-15. Les méthodes et attributs de classe

Supposons qu'on veuille compter le nombre d'objets Personne créées dans une application. On peut soi-même gérer un compteur mais on risque d'oublier les objets temporaires qui sont créés ici ou là. Il semblerait plus sûr d'inclure dans les constructeurs de la classe Personne , une instruction incrémentant un compteur. Le problème est de passer une référence de ce compteur afin que le constructeur puisse l'incrémenter : il faut leur passer un nouveau paramètre. On peut aussi inclure le compteur dans la définition de la classe. Comme c'est un attribut de la classe elle-même et non celui d'une instance particulière de cette classe, on le déclare différemment avec le mot clé static :

Cacher Sélectionnez
  1.         private static long nbPersonnes;

Pour le référencer, on écrit P ersonne.nbPersonnes pour montrer que c'est un attribut de la classe P ersonne elle-même. Ici, nous avons créé un attribut privé auquel on n'aura pas accès directement en-dehors de la classe. On crée donc une propriété publique pour donner accès à l'attribut de classe nbPersonnes . Pour rendre la valeur de nbPersonnes la méthode get de cette propriété n'a pas besoin d'un objet P ersonne particulier : en effet nbPersonnes est l'attribut de toute une classe. Aussi a-t-on besoin d'une propriété déclarée elle-aussi static :

Cacher Sélectionnez
  1.         public static long NbPersonnes {
  2.             get { return nbPersonnes; }
  3. }

qui de l'extérieur sera appelée avec la syntaxe P ersonne.NbPersonnes . Voici un exemple.

La classe P ersonne devient la suivante :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     public class Personne {
  5.
  6.         // attributs de classe
  7.         private static long nbPersonnes;
  8.         public static long NbPersonnes {
  9.             get { return nbPersonnes; }
  10.         }
  11.
  12.         // attributs d'instance
  13.         private string prenom;
  14.         private string nom;
  15.         private int age;
  16.
  17.         // constructeurs
  18.         public Personne(String p, String n, int age) {
  19.             Initialise(p, n, age);
  20.             nbPersonnes++;
  21.         }
  22.         public Personne(Personne p) {
  23.             Initialise(p);
  24.             nbPersonnes++;
  25.         }
  26.
  27. ...
  28. }

Lignes 20 et 24, les constructeurs incrémentent le champ statique de la ligne 7.

Avec le programme suivant :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program {
  5.         static void Main(string[] args) {
  6.             Personne p1 = new Personne("Jean", "Dupont", 30);
  7.             Personne p2 = new Personne(p1);
  8.             new Personne(p1);
  9.             Console.WriteLine("Nombre de personnes créées : " + Personne.NbPersonnes);
  10.         }
  11.     }
  12. }

on obtient les résultats suivants :

Cacher Sélectionnez
  1.     Nombre de personnes créées : 3

V-A-16. Un tableau de personnes

Un objet est une donnée comme une autre et à ce titre plusieurs objets peuvent être rassemblés dans un tableau :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program {
  5.         static void Main(string[] args) {
  6.             // un tableau de personnes
  7.             Personne[] amis = new Personne[3];
  8.             amis[0] = new Personne("Jean", "Dupont", 30);
  9.             amis[1] = new Personne("Sylvie", "Vartan", 52);
  10.             amis[2] = new Personne("Neil", "Armstrong", 66);
  11.             // affichage
  12.             foreach (Personne ami in amis) {
  13.                 ami.Identifie();
  14.             }
  15.         }
  16.     }
  17. }
  • ligne 7 : crée un tableau de 3 éléments de type P ersonne . Ces 3 éléments sont initialisés ici avec la valeur null , c.a.d. qu'ils ne référencent aucun objet. De nouveau, par abus de langage, on parle de tableau d'objets alors que ce n'est qu'un tableau de références d'objets. La création du tableau d'objets, qui est un objet lui-même (présence de new ) ne crée aucun objet du type de ses éléments : il faut le faire ensuite.
  • lignes 8-10 : création des 3 objets de type Personne
  • lignes 12-14 : affichage du contenu du tableau amis

On obtient les résultats suivants :

Cacher Sélectionnez
  1. [Jean, Dupont, 30]
  2. [Sylvie, Vartan, 52]
  3. [Neil, Armstrong, 66]

V-B. L'héritage par l'exemple

V-B-1. Généralités

Nous abordons ici la notion d'héritage. Le but de l'héritage est de "personnaliser" une classe existante pour qu'elle satisfasse à nos besoins. Supposons qu'on veuille créer une classe Enseignant : un enseignant est une personne particulière. Il a des attributs qu'une autre personne n'aura pas : la matière qu'il enseigne par exemple. Mais il a aussi les attributs de toute personne : prénom, nom et âge. Un enseignant fait donc pleinement partie de la classe Personne mais a des attributs supplémentaires. Plutôt que d'écrire une classe Enseignant à partir de rien, on préfèrerait reprendre l'acquis de la classe Personne qu'on adapterait au caractère particulier des enseignants. C'est le concept d'héritage qui nous permet cela.

Pour exprimer que la classe Enseignant hérite des propriétés de la classe Personne , on écrira :

Cacher Sélectionnez
  1.     public class Enseignant : Personne

Personne est appelée la classe parent (ou mère) et Enseignant la classe dérivée (ou fille). Un objet Enseignant a toutes les qualités d'un objet Personne : il a les mêmes attributs et les mêmes méthodes. Ces attributs et méthodes de la classe parent ne sont pas répétées dans la définition de la classe fille : on se contente d'indiquer les attributs et méthodes rajoutés par la classe fille :

Nous supposons que la classe Personne est définie comme suit :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     public class Personne {
  5.
  6.         // attributs de classe
  7.         private static long nbPersonnes;
  8.         public static long NbPersonnes {
  9.             get { return nbPersonnes; }
  10.         }
  11.
  12.         // attributs d'instance
  13.         private string prenom;
  14.         private string nom;
  15.         private int age;
  16.
  17.         // constructeurs
  18.         public Personne(String prenom, String nom, int age) {
  19.             Nom = nom;
  20.             Prenom = prenom;
  21.             Age = age;
  22.             nbPersonnes++;
  23.             Console.WriteLine("Constructeur Personne(string, string, int)");
  24.         }
  25.         public Personne(Personne p) {
  26.             Nom = p.Nom;
  27.             Prenom = p.Prenom;
  28.             Age = p.Age;
  29.             nbPersonnes++;
  30.             Console.WriteLine("Constructeur Personne(Personne)");
  31.         }
  32.
  33.         // propriétés
  34.         public string Prenom {
  35.             get { return prenom; }
  36.             set {
  37.                 // prénom valide ?
  38.                 if (value == null || value.Trim().Length == 0) {
  39.                     throw new Exception("prénom (" + value + ") invalide");
  40.                 } else {
  41.                     prenom = value;
  42.                 }
  43.             }//if
  44.         }//prenom
  45.
  46.         public string Nom {
  47.             get { return nom; }
  48.             set {
  49.                 // nom valide ?
  50.                 if (value == null || value.Trim().Length == 0) {
  51.                     throw new Exception("nom (" + value + ") invalide");
  52.                 } else { nom = value; }
  53.             }//if
  54.         }//nom
  55.
  56.         public int Age {
  57.             get { return age; }
  58.             set {
  59.                 // age valide ?
  60.                 if (value >= 0) {
  61.                     age = value;
  62.                 } else
  63.                     throw new Exception("âge (" + value + ") invalide");
  64.             }//if
  65.         }//age
  66.
  67.         // propriété
  68.         public string Identite {
  69.             get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age);}
  70.         }
  71.     }
  72.
  73. }

La méthode Identifie a été remplacée par la propriété Identite en lecture seule et qui identifie la personne. Nous créons une classe Enseignant héritant de la classe Personne :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Enseignant : Personne {
  5.         // attributs
  6.         private int section;
  7.
  8.         // constructeur
  9.         public Enseignant(string prenom, string nom, int age, int section)
  10.             : base(prenom, nom, age) {
  11.             // on mémorise la section via la propriété Section
  12.             Section = section;
  13.             // suivi
  14.             Console.WriteLine("Construction Enseignant(string, string, int, int)");
  15.         }//constructeur
  16.
  17.         // propriété Section
  18.         public int Section {
  19.             get { return section; }
  20.             set { section = value; }
  21.         }// Section
  22.
  23.     }
  24. }

La classe Enseignant rajoute aux méthodes et attributs de la classe Personne :

  • ligne 4 : la classe Enseignant dérive de la classe Personne
  • ligne 6 : un attribut section qui est le n° de section auquel appartient l'enseignant dans le corps des enseignants (une section par discipline en gros). Cet attribut privé est accessible via la propriété publique Section des lignes 18-21
  • ligne 9 : un nouveau constructeur permettant d'initialiser tous les attributs d'un enseignant

V-B-2. Construction d'un objet Enseignant

Une classe fille n'hérite pas des constructeurs de sa classe Parent. Elle doit alors définir ses propres constructeurs. Le constructeur de la classe Enseignant est le suivant :

Cacher Sélectionnez
  1.         // constructeur
  2.         public Enseignant(string prenom, string nom, int age, int section)
  3.             : base(prenom, nom, age) {
  4.             // on mémorise la section
  5.             Section = section;
  6.             // suivi
  7.             Console.WriteLine("Construction enseignant(string, string, int, int)");
  8. }//constructeur

La déclaration

Cacher Sélectionnez
  1.         public Enseignant(string prenom, string nom, int age, int section)
  2.             : base(prenom, nom, age) {

déclare que le constructeur reçoit quatre paramètres prenom , nom , age , section et en passe trois (prenom,nom,age) à sa classe de base, ici la classe Personne . On sait que cette classe a un constructeur Personne(string, string, int) qui va permettre de construire une personne avec les paramètres passsés (prenom,nom,age) . Une fois la construction de la classe de base terminée, la construction de l'objet Enseignant se poursuit par l'exécution du corps du constructeur :

Cacher Sélectionnez
  1.             // on mémorise la section
  2.             Section = section;

On notera qu'à gauche du signe =, ce n'est pas l'attribut section de l'objet qui a été utilisé, mais la propriété Section qui lui est associée. Cela permet au constructeur de profiter des éventuels contrôles de validité que pourrait faire cette méthode. Cela évite de placer ceux-ci à deux endroits différents : le constructeur et la propriété.

En résumé, le constructeur d'une classe dérivée :

  • passe à sa classe de base les paramètres dont celle-ci a besoin pour se construire
  • utilise les autres paramètres pour initialiser les attributs qui lui sont propres

On aurait pu préférer écrire :

Cacher Sélectionnez
  1. // constructeur
  2.   public Enseignant(string prenom, string nom, int age, int section){
  3.     this.prenom=prenom;
  4.         this.nom=nom;
  5.         this.age=age;
  6.       this.section=section;
  7.   }

C'est impossible. La classe Personne a déclaré privés ( private ) ses trois champs prenom , nom et age . Seuls des objets de la même classe ont un accès direct à ces champs. Tous les autres objets, y compris des objets fils comme ici, doivent passer par des méthodes publiques pour y avoir accès. Cela aurait été différent si la classe Personne avait déclaré protégés ( protected ) les trois champs : elle autorisait alors des classes dérivées à avoir un accès direct aux trois champs. Dans notre exemple, utiliser le constructeur de la classe parent était donc la bonne solution et c'est la méthode habituelle : lors de la construction d'un objet fils, on appelle d'abord le constructeur de l'objet parent puis on complète les initialisations propres cette fois à l'objet fils ( section dans notre exemple).

Tentons un premier programme de test [Program.cs] :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program {
  5.         static void Main(string[] args) {
  6.             Console.WriteLine(new Enseignant("Jean", "Dupont", 30, 27).Identite);
  7.         }
  8.     }
  9. }

Ce programme ce contente de créer un objet Enseignant (new) et de l'identifier. La classe Enseignant n'a pas de méthode I dentite mais sa classe parent en a une qui de plus est publique : elle devient par héritage une méthode publique de la classe E nseignant .

L'ensemble du projet est le suivant :

Image non disponible

Les résultats obtenus sont les suivants :

Cacher Sélectionnez
  1. Constructeur Personne(string, string, int)
  2. Construction Enseignant(string, string, int, int)
  3. [Jean, Dupont, 30]

On voit que :

  • un objet P ersonne (ligne 1) a été construit avant l'objet E nseignant (ligne 2)
  • l'identité obtenue est celle de l'objet Personne

V-B-3. Redéfinition d'une méthode ou d'une propriété

Dans l'exemple précédent, nous avons eu l'identité de la partie P ersonne de l'enseignant mais il manque certaines informations propres à la classe Enseignant (la section). On est donc amené à écrire une propriété permettant d'identifier l'enseignant :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Enseignant : Personne {
  5.         // attributs
  6.         private int section;
  7.
  8.         // constructeur
  9.         public Enseignant(string prenom, string nom, int age, int section)
  10.             : base(prenom, nom, age) {
  11.             // on mémorise la section via la propriété Section
  12.             Section = section;
  13.             // suivi
  14.             Console.WriteLine("Construction Enseignant(string, string, int, int)");
  15.         }//constructeur
  16.
  17.         // propriété Section
  18.         public int Section {
  19.             get { return section; }
  20.             set { section = value; }
  21.         }// section
  22.
  23.         // propriété Identite
  24.         public new string Identite {
  25.             get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); }
  26.         }
  27.     }
  28. }

Lignes 24-26, la propriété Identite de la classe Enseignant s'appuie sur la propriété Identite de sa classe mère ( base.Identite ) (ligne 25) pour afficher sa partie "P ersonne " puis complète avec le champ section qui est propre à la classe Enseignant . Notons la déclaration de la propriété Identite :

Cacher Sélectionnez
  1.     public new string Identite{

Soit un objet enseignant E. Cet objet contient en son sein un objet Personne :

Image non disponible

La propriété Identite est définie à la fois dans la classe Enseignant et sa classe mère Personne . Dans la classe fille Enseignant , la propriété Identite doit être précédée du mot clé new pour indiquer qu'on redéfinit une nouvelle propriété Identite pour la classe Enseignant.

Cacher Sélectionnez
  1.     public new string Identite{

La classe E nseignant dispose maintenant de deux propriétés I dentite :

  • celle héritée de la classe parent P ersonne
  • la sienne propre

Si E est un ojet E nseignant , E.Identite désigne la propriété I dentite de la classe E nseignant . On dit que la propriété I dentite de la classe fille redéfinit ou cache la propriété I dentite de la classe mère. De façon générale, si O est un objet et M une méthode, pour exécuter la méthode O.M , le système cherche une méthode M dans l'ordre suivant :

  • dans la classe de l'objet O
  • dans sa classe mère s'il en a une
  • dans la classe mère de sa classe mère si elle existe
  • etc…

L'héritage permet donc de redéfinir dans la classe fille des méthodes/propriétés de même nom dans la classe mère. C'est ce qui permet d'adapter la classe fille à ses propres besoins. Associée au polymorphisme que nous allons voir un peu plus loin, la redéfinition de méthodes/propriétés est le principal intérêt de l'héritage.

Considérons le même programme de test que précédemment :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program {
  5.         static void Main(string[] args) {
  6.             Console.WriteLine(new Enseignant("Jean", "Dupont", 30, 27).Identite);
  7.         }
  8.     }
  9. }

Les résultats obtenus sont cette fois les suivants :

Cacher Sélectionnez
  1. Constructeur Personne(string, string, int)
  2. Construction Enseignant(string, string, int, int)
  3. Enseignant[[Jean, Dupont, 30],27]

V-B-4. Le polymorphisme

Considérons une lignée de classes : C0 ← C1 ← C2 ← … ←Cn

où Ci ← Cj indique que la classe Cj est dérivée de la classe Ci . Cela entraîne que la classe Cj a toutes les caractéristiques de la classe Ci plus d'autres. Soient des objets Oi de type Ci. Il est légal d'écrire :

Oi=Oj avec j>i

En effet, par héritage, la classe Cj a toutes les caractéristiques de la classe Ci plus d'autres. Donc un objet Oj de type Cj contient en lui un objet de type Ci. L'opération

Oi=Oj

fait que Oi est une référence à l'objet de type Ci contenu dans l'objet Oj.

Le fait qu'une variable Oi de classe Ci puisse en fait référencer non seulement un objet de la classe Ci mais en fait tout objet dérivé de la classe Ci, est appelé polymorphisme : la faculté pour une variable de référencer différents types d'objets.

Prenons un exemple et considérons la fonction suivante indépendante de toute classe ( static ):

Cacher Sélectionnez
  1.     public static void Affiche(Personne p){
  2.         ….
  3.     }

On pourra aussi bien écrire

Cacher Sélectionnez
  1.     Personne p;
  2.     ...
  3.     Affiche(p);

que

Cacher Sélectionnez
  1.
  2.     Enseignant e;
  3.     ...
  4.     Affiche(e);

Dans ce dernier cas, le paramètre formel p de type Personne de la méthode statique A ffiche va recevoir une valeur de type Enseignant . Comme le type Enseignant dérive du type Personne , c'est légal.

V-B-5. Redéfinition et polymorphisme

Complétons notre méthode A ffiche :

Cacher Sélectionnez
  1.         public static void Affiche(Personne p) {
  2.             // affiche identité de p
  3.             Console.WriteLine(p.Identite);
  4. }//affiche

La propriété p.Identite rend une chaîne de caractères identifiant l'objet Personne p . Que se passe-t-il dans l'exemple précédent si le paramètre passé à la méthode Affiche est un objet de type Enseignant :

Cacher Sélectionnez
  1.             Enseignant e = new Enseignant(...);
  2.             Affiche(e);

Regardons l'exemple suivant :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program2 {
  5.         static void Main(string[] args) {
  6.             // un enseignant
  7.             Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
  8.             Affiche(e);
  9.             // une personne
  10.             Personne p = new Personne("Jean", "Dupont", 30);
  11.             Affiche(p);
  12.         }
  13.
  14.         // affiche
  15.         public static void Affiche(Personne p) {
  16.             // affiche identité de p
  17.             Console.WriteLine(p.Identite);
  18.         }//affiche
  19.     }
  20. }

Les résultats obtenus sont les suivants :

Cacher Sélectionnez
  1. Constructeur Personne(string, string, int)
  2. Construction Enseignant(string, string, int, int)
  3. [Lucile, Dumas, 56]
  4. Constructeur Personne(string, string, int)
  5. [Jean, Dupont, 30]

L'exécution montre que l'instruction p.Identite (ligne 17) a exécuté à chaque fois la propriété Identite d'une Personne , d'abord (ligne 7) la personne contenue dans l' Enseignant e, puis (ligne 10) la Personne p elle-même. Elle ne s'est pas adaptée à l'objet réellement passé en paramètre à Affiche . On aurait préféré avoir l'identité complète de l' Enseignant e. Il aurait fallu pour cela que la notation p.Identite référence la propriété Identite de l'objet réellement pointé par p plutôt que la propriété Identite de partie " Personne " de l'objet réellement par p .

Il est possible d'obtenir ce résultat en déclarant Identite comme une propriété virtuelle (virtual) dans la classe de base Personne :

Cacher Sélectionnez
  1. public virtual string Identite {
  2.             get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age); }
  3.         }

Le mot clé virtual fait de Identite une propriété virtuelle. Ce mot clé peut s'appliquer également aux méthodes. Les classes filles qui redéfinissent une propriété ou méthode virtuelle doivent alors utiliser le mot clé override au lieu de new pour qualifier leur propriété/méthode redéfinie. Ainsi dans la classe Enseignant , la propriété Identite est redéfinie comme suit :

Cacher Sélectionnez
  1.         public override string Identite {
  2.             get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); }
  3. }

Le programme précédent produit alors les résultats suivants :

Cacher Sélectionnez
  1. Constructeur Personne(string, string, int)
  2. Construction Enseignant(string, string, int, int)
  3. Enseignant[[Lucile, Dumas, 56],61]
  4. Constructeur Personne(string, string, int)
  5. [Jean, Dupont, 30]

Cette fois-ci, ligne 3, on a bien eu l'identité complète de l'enseignant. Redéfinissons maintenant une méthode plutôt qu'une propriété. La classe o bject (alias C# de System.Object ) est la classe "mère" de toutes les classes C#. Ainsi lorsqu'on écrit :

Cacher Sélectionnez
  1.     public class Personne

on écrit implicitement :

Cacher Sélectionnez
  1.     public class Personne : System.Object

La classe System.Object définit une méthode virtuelle ToString :

Image non disponible

La méthode ToString rend le nom de la classe à laquelle appartient l'objet comme le montre l'exemple suivant :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program2 {
  5.         static void Main(string[] args) {
  6.             // un enseignant
  7.             Console.WriteLine(new Enseignant("Lucile", "Dumas", 56, 61).ToString());
  8.             // une personne
  9.             Console.WriteLine(new Personne("Jean", "Dupont", 30).ToString());
  10.         }
  11.     }
  12. }

Les résultats produits sont les suivants :

Cacher Sélectionnez
  1. Constructeur Personne(string, string, int)
  2. Construction Enseignant(string, string, int, int)
  3. Chap2.Enseignant
  4. Constructeur Personne(string, string, int)
  5. Chap2.Personne

On remarquera que bien que nous n'ayons pas redéfini la méthode ToString dans les classes Personne et Enseignant , on peut cependant constater que la méthode ToString de la classe Object a été capable d'afficher le nom réel de la classe de l'objet.

Redéfinissons la méthode ToString dans les classes Personne et Enseignant :

Cacher Sélectionnez
  1.         // méthode ToString
  2.         public override string ToString() {
  3.             return Identite;
  4. }

La définition est la même dans les deux classes. Considérons le programme de test suivant :

Cacher Sélectionnez
  1. using System;
  2. namespace Chap2 {
  3.     class Program3 {
  4.         public static void Main() {
  5.             // un enseignant
  6.             Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
  7.             Affiche(e);
  8.             // une personne
  9.             Personne p = new Personne("Jean", "Dupont", 30);
  10.             Affiche(p);
  11.         }
  12.         // affiche
  13.         public static void Affiche(Personne p) {
  14.             // affiche identité de p
  15.             Console.WriteLine(p);
  16.         }//Affiche
  17.     }
  18. }

Attardons-nous sur la méthode Affiche qui admet pour paramètre une personne p . Ligne 15, la méthode WriteLine de la classe Console n'a aucune variante admettant un paramètre de type Personne . Parmi les différentes variantes de Writeline , il en existe une qui admet comme paramètre un type Object . Le compilateur va utiliser cette méthode, WriteLine(Object o), parce que cette signature signifie que le paramètre o peut être de type Object ou dérivé. Puisque Object est la classe mère de toutes les classes, tout objet peut être passé en paramètre à WriteLine et donc un objet de type Personne ou Enseignant . La méthode WriteLine(Object o) écrit o.ToString() dans le flux d'écriture Out . La méthode ToString étant virtuelle, si l'objet o (de type Object ou dérivé) a redéfini la méthode ToString , ce sera cette dernière qui sera utilisée. C'est ici le cas avec les classes Personne et Enseignant .

C'est ce que montrent les résultats d'exécution :

Cacher Sélectionnez
  1. Constructeur Personne(string, string, int)
  2. Construction Enseignant(string, string, int, int)
  3. Enseignant[[Lucile, Dumas, 56],61]
  4. Constructeur Personne(string, string, int)
  5. [Jean, Dupont, 30]

V-C. Redéfinir la signification d'un opérateur pour une classe

V-C-1. Introduction

Considérons l'instruction

op1 + op2

où op1 et op2 sont deux opérandes. Il est possible de redéfinir la signification de l'opérateur + . Si l'opérande op1 est un objet de classe C1, il faut définir une méthode statique dans la classe C1 avec la signature suivante :

Cacher Sélectionnez
  1. public static [type] operator +(C1 opérande1, C2 opérande2);

Lorsque le compilateur rencontre l'instruction

op1 + op2

il la traduit alors par C1.operator+(op1,op2) . Le type rendu par la méthode operator est important. En effet, considérons l'opération op1+op2+op3 . Elle est traduite par le compilateur par (op1+op2)+op3 . Soit res12 le résultat de op1+op2 . L'opération qui est faite ensuite est res12+op3 . Si res12 est de type C1 , elle sera traduite elle aussi par C1.operator+(res12,op3) . Cela permet d'enchaîner les opérations.

On peut redéfinir également les opérateurs unaires n'ayant qu'un seul opérande. Ainsi si op1 est un objet de type C1 , l'opération op1++ peut être redéfinie par une méthode statique de la classe C1 :

Cacher Sélectionnez
  1. public static [type] operator ++(C1 opérande1);

Ce qui a été dit ici est vrai pour la plupart des opérateurs avec cependant quelques exceptions :

  • les opérateurs == et != doivent être redéfinis en même temps
  • les opérateurs && ,||, [], (), +=, -=… ne peuvent être redéfinis

V-C-2. Un exemple

On crée une classe ListeDePersonnes dérivée de la classe ArrayList . Cette classe implémente une liste dynamique et est présentée dans le chapitre qui suit. De cette classe, nous n'utilisons que les éléments suivants :

  • la méthode L. Add(Object o) permettant d'ajouter à la liste L un objet o . Ici l'objet o sera un objet Personne .
  • la propriété L. Count qui donne le nombre d'éléments de la liste L
  • la notation L [i] qui donne l'élément i de la liste L

La classe ListeDePersonnes va hériter de tous les attributs, méthodes et propriétés de la classe ArrayList . Sa définition est la suivante :

Cacher Sélectionnez
  1. using System;
  2. using System.Collections;
  3. using System.Text;
  4.
  5. namespace Chap2 {
  6.     class ListeDePersonnes : ArrayList{
  7.         // redéfinition opérateur +, pour ajouter une personne à la liste
  8.         public static ListeDePersonnes operator +(ListeDePersonnes l, Personne p) {
  9.             // on ajoute la Personne p à la ListeDePersonnes l
  10.             l.Add(p);
  11.             // on rend la ListeDePersonnes l
  12.             return l;
  13.         }// operator +
  14.
  15.         // ToString
  16.         public override string ToString() {
  17.             // rend (él1, él2, ..., éln)
  18.             // parenthèse ouvrante
  19.             StringBuilder listeToString = new StringBuilder("(");
  20.             // on parcourt la liste de personnes (this)
  21.             for (int i = 0; i < Count - 1; i++) {
  22.                 listeToString.Append(this[i]).Append(",");
  23.             }//for
  24.             // dernier élément
  25.             if (Count != 0) {
  26.                 listeToString.Append(this[Count-1]);
  27.             }
  28.             // parenthèse fermante
  29.             listeToString.Append(")");
  30.             // on doit rendre un string
  31.             return listeToString.ToString();
  32.         }//ToString
  33.     }
  34. }
  • ligne 6 : la classe ListeDePersonnes dérive de la classe ArrayList
  • lignes 8-13 : définition de l'opérateur + pour l'opération l + p, où l est de type ListeDePersonnes et p de type Personne ou dérivé.
  • ligne 10 : la personne p est ajoutée à la liste l. C'est la méthode Add de la classe parent ArrayList qui est ici utilisée.
  • ligne 12 : la référence sur la liste l est rendue afin de pouvoir enchaîner les opérateurs + tels que dans l + p1 + p2. L'opération l+p1+p2 sera interprétée (priorité des opérateurs) comme (l+p1)+p2 . L'opération l+p1 rendra la référence l . L'opération (l+p1)+p2 devient alors l+p2 qui ajoute la personne p2 à la liste de personnes l .
  • ligne 16 : nous redéfinissons la méthode ToString afin d'afficher une liste de personnes sous la forme (personne1, personne2, ..) où personnei est lui-même le résultat de la méthode ToString de la classe Personne .
  • ligne 19 : nous utilisons un objet de type StringBuilder . Cette classe convient mieux que la classe string dès qu'il faut faire de nombreuses opérations sur la chaîne de caractères, ici des ajouts. En effet, chaque opération sur un objet string rend un nouvel objet string , alors que les mêmes opérations sur un objet StringBuilder modifient l'objet mais n'en créent pas un nouveau. Nous utilisons la méthode Append pour concaténer les chaînes de caractères.
  • ligne 21 : on parcourt les éléments de la liste de personnes. Cette liste est ici désignée par this. C'est l'objet courant sur laquelle est exécutée la méthode ToString . La propriété Count est une propriété de la classe parent ArrayList .
  • ligne 22 : l'élément n° i de la liste courante this est accessible via la notation this[i] . Là encore, c'est une propriété de la classe ArrayList . Comme il s'agit d'ajouter des chaînes, c'est la méthode this[i].ToString() qui va être utilisée. Comme cette méthode est virtuelle, c'est la méthode ToString de l'objet this , de type Personne ou dérivé, qui va être utilisée.
  • ligne 31 : il nous faut rendre un objet de type string (ligne 16). La classe StringBuilder a une méthode ToString qui permet de passer d'un type StringBuilder à un type string .

On notera que la classe ListeDePersonnes n'a pas de constructeur. Dans ce cas, on sait que le constructeur

Cacher Sélectionnez
  1. public ListeDePersonnes(){
  2. }

sera utilisé. Ce constructeur ne fait rien si ce n'est appeler le constructeur sans paramètres de sa classe parent :

Cacher Sélectionnez
  1. public ArrayList(){
  2. ...
  3. }

Une classe de test pourrait être la suivante :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program1 {
  5.         static void Main(string[] args) {
  6.             // une liste de personnes
  7.             ListeDePersonnes l = new ListeDePersonnes();
  8.             // ajout de personnes
  9.             l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
  10.             // affichage
  11.             Console.WriteLine("l=" + l);
  12.             l = l + new Enseignant("camille", "germain",27,60);
  13.             Console.WriteLine("l=" + l);
  14.         }
  15.     }
  16. }
  • ligne 7 : création d'une liste de personnes l
  • ligne 9 : ajout de 2 personnes avec l'opérateur +
  • ligne 12 : ajout d'un enseignant
  • lignes 11 et 13 : utilisation de la méthode redéfinie ListeDePersonnes.ToString() .

Les résultats :

Cacher Sélectionnez
  1. l=([jean, martin, 10],[pauline, leduc, 12])
  2. l=([jean, martin, 10],[pauline, leduc, 12],Enseignant[[camille, germain, 27],60])

V-D. Définir un indexeur pour une classe

Nous continuons ici à utiliser la classe ListeDePersonnes . Si l est un objet ListeDePersonnes , nous souhaitons pouvoir utiliser la notation l[i] pour désigner la personne n° i de la liste l aussi bien en lecture ( Personne p=l[i] ) qu'en écriture ( l[i]=new Personne(…) ).

Pour pouvoir écrire l[i] où l[i] désigne un objet Personne , il nous faut définir dans la classe ListeDePersonnes la méthode this suivante :

Cacher Sélectionnez
  1.         public Personne this[int i] {
  2.             get { ... }
  3.             set { ... }
  4. }

On appelle la méthode this[int i] , un indexeur car elle donne une signification à l'expression obj[i] qui rappelle la notation des tableaux alors que obj n'est pas un tableau mais un objet. La méthode get de la méthode this de l'objet obj est appelée lorsqu'on écrit variable=obj[i] et la méthode set lorsqu'on écrit obj[i]=valeur .

La classe ListeDePersonnes dérive de la classe ArrayList qui a elle-même un indexeur :

Cacher Sélectionnez
  1.     public object this[int i] { ... }

Il y a un conflit entre la méthode this de la classe ListeDePersonnes :

Cacher Sélectionnez
  1. public Personne this[int i]

et la méthode this de la classe ArrayList

Cacher Sélectionnez
  1. public object this[int i]

parce qu'elles portent le même nom et admettent le même type de paramètre (int).Pour indiquer que la méthode this de la classe ListeDePersonnes "cache" la méthode de même nom de la classe ArrayList, on est obligé d'ajouter le mot clé new à la déclaration de l'indexeur de ListeDePersonnes . On écrira donc :

Cacher Sélectionnez
  1.     public new Personne this[int i]{
  2.         get { ... }
  3.         set { ... }
  4.     }

Complétons cette méthode. La méthode this.get est appelée lorsqu'on écrit variable=l[i] par exemple, où l est de type ListeDePersonnes . On doit alors retourner la personne n° i de la liste l . Ceci se fait avec la notation base[i], qui rend l'objet n° i de la classe ArrayList sous-jacente à la classe ListeDePersonnes . L'objet retourné étant de type Object , un transtypage vers la classe Personne est nécessaire.

Cacher Sélectionnez
  1.     public new Personne this[int i]{
  2.         get { return (Personne) base[i]; }
  3.         set { ... }
  4.     }

La méthode set est appelée lorsqu'on écrit l[i]=p où p est une Personne . Il s'agit alors d'affecter la personne p à l'élément i de la liste l .

Cacher Sélectionnez
  1.     public new Personne this[int i]{
  2.         get { ... }
  3.         set { base[i]=value; }
  4.     }

Ici, la personne p représentée par le mot clé value est affectée à l'élément n° i de la classe de base ArrayList .

L'indexeur de la classe ListeDePersonnes sera donc le suivant :

Cacher Sélectionnez
  1.     public new Personne this[int i]{
  2.         get { return (Personne) base[i]; }
  3.         set { base[i]=value; }
  4.     }

Maintenant, on veut pouvoir écrire également Personne p=l["nom"] , c.a.d indexer la liste l non plus par un n° d'élément mais par un nom de personne. Pour cela on définit un nouvel indexeur :

Cacher Sélectionnez
  1.         // indexeur via un nom
  2.         public int this[string nom] {
  3.             get {
  4.                 // on recherche la personne
  5.                 for (int i = 0; i < Count; i++) {
  6.                     if (((Personne)base[i]).Nom == nom)
  7.                         return i;
  8.                 }//for
  9.                 return -1;
  10.             }//get
  11. }

La première ligne

Cacher Sélectionnez
  1. public int this[string nom]

indique qu'on indexe la classe ListeDePersonnes par une chaîne de caractères nom et que le résultat de l[nom] est un entier. Cet entier sera la position dans la liste, de la personne portant le nom nom ou -1 si cette personne n'est pas dans la liste. On ne définit que la propriété get, interdisant ainsi l'écriture l["nom"]=valeur qui aurait nécessité la définition de la propriété set . Le mot clé new n'est pas nécessaire dans la déclaration de l'indexeur car la classe de base ArrayList ne définit pas d'indexeur this[string] .

Dans le corps du get , on parcourt la liste des personnes à la recherche du nom passé en paramètre. Si on le trouve en position i, on renvoie i sinon on renvoie -1.

Le programme de test précédent est complété de la façon suivante :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program2 {
  5.         static void Main(string[] args) {
  6.             // une liste de personnes
  7.             ListeDePersonnes l = new ListeDePersonnes();
  8.             // ajout de personnes
  9.             l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
  10.             // affichage
  11.             Console.WriteLine("l=" + l);
  12.             l = l + new Enseignant("camille", "germain",27,60);
  13.             Console.WriteLine("l=" + l);
  14.             // changement élément 1
  15.             l[1] = new Personne("franck", "gallon",5);
  16.             // affichage élément 1
  17.             Console.WriteLine("l[1]=" + l[1]);
  18.             // affichage liste l
  19.             Console.WriteLine("l=" + l);
  20.             // recherche de personnes
  21.             string[] noms = { "martin", "germain", "xx" };
  22.             for (int i = 0; i < noms.Length; i++) {
  23.                 int inom = l[noms[i]];
  24.                 if (inom != -1)
  25.                     Console.WriteLine("Personne(" + noms[i] + ")=" + l[inom]);
  26.                 else
  27.                     Console.WriteLine("Personne(" + noms[i] + ") n'existe pas");
  28.             }//for
  29.         }
  30.     }
  31. }

Son exécution donne les résultats suivants :

Cacher Sélectionnez
  1. l=([jean, martin, 10],[pauline, leduc, 12])
  2. l=([jean, martin, 10],[pauline, leduc, 12],Enseignant[[camille, germain, 27],60])
  3. l[1]=[franck, gallon, 5]
  4. l=([jean, martin, 10],[franck, gallon, 5],Enseignant[[camille, germain, 27],60])
  5. Personne(martin)=[jean, martin, 10]
  6. Personne(germain)=Enseignant[[camille, germain, 27],60]
  7. Personne(xx) n'existe pas

V-E. Les structures

La structure C# est analogue à la structure du langage C et est très proche de la notion de classe. Une structure est définie comme suit :

Cacher Sélectionnez
  1. struct NomStructure{
  2. // attributs
  3.     ...
  4. // propriétés
  5. ...
  6. // constructeurs
  7. ...
  8. // méthodes
  9. ...
  10. }

Il y a, malgré une similitude de déclaration des différences importantes entre classe et structure. La notion d'héritage n'existe par exemple pas avec les structures. Si on écrit une classe qui ne doit pas être dérivée, quelles sont les différences entre structure et classe qui vont nous aider à choisir entre les deux ? Aidons-nous de l'exemple suivant pour le découvrir :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program1 {
  5.         static void Main(string[] args) {
  6.             // une structure sp1
  7.             SPersonne sp1;
  8.             sp1.Nom = "paul";
  9.             sp1.Age = 10;
  10.             Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
  11.             // une structure sp2
  12.             SPersonne sp2 = sp1;
  13.             Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");
  14.             // sp2 est modifié
  15.             sp2.Nom = "nicole";
  16.             sp2.Age = 30;
  17.             // vérification sp1 et sp2
  18.             Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
  19.             Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");
  20.
  21.             // un objet op1
  22.             CPersonne op1=new CPersonne();
  23.             op1.Nom = "paul";
  24.             op1.Age = 10;
  25.             Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
  26.             // un objet op2
  27.             CPersonne op2=op1;
  28.             Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
  29.             // op2 est modifié
  30.             op2.Nom = "nicole";
  31.             op2.Age = 30;
  32.             // vérification op1 et op2
  33.             Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
  34.             Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
  35.         }
  36.     }
  37.     // structure SPersonne
  38.     struct SPersonne {
  39.         public string Nom;
  40.         public int Age;
  41.     }
  42.
  43.     // classe CPersonne
  44.     class CPersonne {
  45.         public string Nom;
  46.         public int Age;
  47.     }
  48.
  49. }
  • lignes 38-41 : une structure avec deux champs publics : N om , A ge
  • lignes 44-47 : une classe avec deux champs publics : N om , A ge

Si on exécute ce programme, on obtient les résultats suivants :

Cacher Sélectionnez
  1. sp1=SPersonne(paul,10)
  2. sp2=SPersonne(paul,10)
  3. sp1=SPersonne(paul,10)
  4. sp2=SPersonne(nicole,30)
  5. op1=CPersonne(paul,10)
  6. op2=CPersonne(paul,10)
  7. op1=CPersonne(nicole,30)
  8. op2=CPersonne(nicole,30)

Là où précédemment on utilisait une classe Personne , nous utilisons maintenant une structure SPersonne :

Cacher Sélectionnez
  1.     struct SPersonne {
  2.         public string Nom;
  3.         public int Age;
  4. }

La structure n'a ici pas de constructeur. Elle pourrait en avoir un comme nous le montrerons plus loin. Par défaut, elle dispose toujours du constructeur sans paramètres, ici SPersonne() .

  • ligne 7 du code : la déclaration
Cacher Sélectionnez
  1.     SPersonne sp1;

est équivalente à l'instruction :

Cacher Sélectionnez
  1.     SPersonne sp1=new Spersonne();

Une structure (Nom,Age) est créée et la valeur de sp1 est cette structure elle-même. Dans le cas de la classe, la création de l'objet (Nom,Age) doit se faire explicitement par l'opérateur new (ligne 22) :

Cacher Sélectionnez
  1. CPersonne op1=new CPersonne();

L'instruction précédente crée un objet CPersonne (grosso modo l'équivalent de notre structure) et la valeur de p1 est alors l'adresse (la référence) de cet objet.

Résumons

  • dans le cas de la structure, la valeur de sp1 est la structure elle-même
  • dans le cas de la classe, la valeur de op1 est l'adresse de l'objet créé
Image non disponible

Lorsque dans le programme on écrit ligne 12 :

Cacher Sélectionnez
  1.             SPersonne sp2 = sp1;

une nouvelle structure sp2(Nom,Age) est créée et initialisée avec la valeur de s p1, donc la structure elle-même.

Image non disponible

La structure de sp1 est dupliquée dans sp2 [1]. C'est une recopie de valeur. Considérons maintenant l'instruction, ligne 27 :

Cacher Sélectionnez
  1. CPersonne op2=op1;

Dans le cas des classes, la valeur de op1 est recopiée dans op2, mais comme cette valeur est en fait l'adresse de l'objet, celui-ci n'est pas dupliqué [2].

Dans le cas de la structure [1], si on modifie la valeur de s p2 on ne modifie pas la valeur de s p1 , ce que montre le programme. Dans le cas de l'objet [2], si on modifie l'objet pointé par op2 , celui pointé par op1 est modifié puisque c'est le même. C'est ce que montrent également les résultats du programme.

On retiendra donc de ces explications que :

  • la valeur d'une variable de type structure est la structure elle-même
  • la valeur d'une variable de type objet est l'adresse de l'objet pointé

Une fois cette différence fondamentale comprise, la structure se montre très proche de la classe comme le montre le nouvel exemple suivant :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.
  5.     // structure SPersonne
  6.     struct SPersonne {
  7.         // attributs privés
  8.         private string nom;
  9.         private int age;
  10.
  11.         // propriétés
  12.         public string Nom {
  13.             get { return nom; }
  14.             set { nom = value; }
  15.         }//nom
  16.
  17.         public int Age {
  18.             get { return age; }
  19.             set { age = value; }
  20.         }//age
  21.
  22.         // Constructeur
  23.         public SPersonne(string nom, int age) {
  24.             this.nom = nom;
  25.             this.age = age;
  26.         }//constructeur
  27.
  28.         // ToString
  29.         public override string ToString() {
  30.             return "SPersonne(" + Nom + "," + Age + ")";
  31.         }//ToString
  32.     }//structure
  33. }//namespace
  • lignes 8-9 : deux champs privés
  • lignes 12-20 : les propriétés publiques associées
  • lignes 23-26 : on définit un constructeur. A noter que le constructeur sans paramètres SPersonne () est toujours présent et n'a pas à être déclaré. Sa déclaration est refusée par le compilateur. Dans le constructeur des lignes 23-26, on pourrait être tenté d'initialiser les champs privés nom , age via leurs propriétés publiques Nom , Age . C'est refusé par le compilateur. Les méthodes de la structure ne peuvent être utilisées lors de la construction de celle-ci.
  • lignes 29-31 : redéfinition de la méthode ToString .

Un programme de test pourrait être le suivant :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program1 {
  5.         static void Main(string[] args) {
  6.             // une personne p1
  7.             SPersonne p1=new SPersonne();
  8.             p1.Nom="paul";
  9.             p1.Age= 10;
  10.             Console.WriteLine("p1={0}",p1);
  11.             // une personne p2
  12.             SPersonne p2 = p1;
  13.             Console.WriteLine("p2=" + p2);
  14.             // p2 est modifié
  15.             p2.Nom = "nicole";
  16.             p2.Age = 30;
  17.             // vérification p1 et p2
  18.             Console.WriteLine("p1=" + p1);
  19.             Console.WriteLine("p2=" + p2);
  20.             // une personne p3
  21.             SPersonne p3 = new SPersonne("amandin", 18);
  22.             Console.WriteLine("p3=" + p3);
  23.             // une personne p4
  24.             SPersonne p4 = new SPersonne { Nom = "x", Age = 10 };
  25.             Console.WriteLine("p4=" + p4);
  26.         }
  27.     }
  28. }
  • ligne 7 : on est obligés d'utiliser explicitement le constructeur sans paramètres, ceci parce qu'il existe un autre constructeur dans la structure. Si la structure n'avait eu aucun constructeur, l'instruction
Cacher Sélectionnez
  1.             SPersonne p1;

aurait suffi pour créer une structure vide.

  • lignes 8-9 : la structure est initialisée via ses propriétés publiques
  • ligne 10 : la méthode p1.ToString va être utilisée dans le WriteLine .
  • ligne 21 : création d'une structure avec le constructeur SPersonne(string,int)
  • ligne 24 : création d'une structure avec le constructeur sans paramètres SPersonne() avec, entre accolades, initialisation des champs privés via leurs propriétés publiques.

On obtient les résultats d'exécution suivants :

Cacher Sélectionnez
  1. p1=SPersonne(paul,10)
  2. p2=SPersonne(paul,10)
  3. p1=SPersonne(paul,10)
  4. p2=SPersonne(nicole,30)
  5. p3=SPersonne(amandin,18)
  6. p4=SPersonne(x,10)

La seule différence notable ici entre structure et classe, c'est qu'avec une classe les objets p1 et p2 auraient pointé sur le même objet à la fin du programme.

V-F. Les interfaces

Une interface est un ensemble de prototypes de méthodes ou de propriétés qui forme un contrat. Une classe qui décide d'implémenter une interface s'engage à fournir une implémentation de toutes les méthodes définies dans l'interface. C'est le compilateur qui vérifie cette implémentation.

Voici par exemple la définition de l'interface System.Collections.IEnumerator :

Cacher Sélectionnez
  1. public interface System.Collections.IEnumerator
  2. {
  3.
  4.     // Properties
  5.     Object Current { get; }
  6.
  7.     // Methods
  8.     bool MoveNext();
  9.     void Reset();
  10. }

Les propriétés et méthodes de l'interface ne sont définies que par leurs signatures. Elles ne sont pas implémentées (n'ont pas de code). Ce sont les classes qui implémentent l'interface qui donnent du code aux méthodes et propriétés de l'interface.

Cacher Sélectionnez
  1. public class C : IEnumerator{
  2.     ...
  3.     Object Current{ get {...}}
  4.     bool MoveNext{...}
  5.     void Reset(){...}
  6. }
  • ligne 1 : la classe C implémente la classe IEnumerator . On notera que le signe : utilisé pour l'implémentation d'une interface est le même que celui utilisé pour la dérivation d'une classe.
  • lignes 3-5 : l'implémentation des méthodes et propriétés de l'interface IEnumerator .

Considérons l'interface suivante :

Cacher Sélectionnez
  1. namespace Chap2 {
  2.     public interface IStats {
  3.         double Moyenne { get; }
  4.         double EcartType();
  5.     }
  6. }

L'interface IStats présente :

  • une propriété en lecture seule Moyenne : pour calculer la moyenne d'une série de valeurs
  • une méthode EcartType : pour en calculer l'écart-type

On notera qu'il n'est nulle part précisé de quelle série de valeurs il s'agit. Il peut s'agir de la moyenne des notes d'une classe, de la moyenne mensuelle des ventes d'un produit particulier, de la température moyenne dans un lieu donné… C'est le principe des interfaces : on suppose l'existence de méthodes dans l'objet mais pas celle de données particulières.

Une première classe d'implémentation de l'interface IStats pourrait une classe servant à mémoriser les notes des élèves d'une classe dans une matière donnée. Un élève serait caractérisé par la structure Elève suivante :

Cacher Sélectionnez
  1.     public struct Elève {
  2.         public string Nom { get; set; }
  3.         public string Prénom { get; set; }
  4. }//Elève

L'élève serait identifié par son nom et son prénom. Lignes 2-3, on trouve les propriétés automatiques pour ces deux attributs.

Une note serait caractérisée par la structure Note suivante :

Cacher Sélectionnez
  1.     public struct Note {
  2.         public Elève Elève { get; set; }
  3.         public double Valeur { get; set; }
  4. }//Note

La note serait identifiée par l'élève noté et la note elle-même. Lignes 2-3, on trouve les propriétés automatiques pour ces deux attributs.

Les notes de tous les élèves dans une matière donnée sont rassemblées dans la classe TableauDeNotes suivante :

Cacher Sélectionnez
  1. using System;
  2. using System.Text;
  3.
  4. namespace Chap2 {
  5.
  6.     public class TableauDeNotes : IStats {
  7.         // attributs
  8.         public string Matière { get; set; }
  9.         public Note[] Notes { get; set; }
  10.         public double Moyenne { get; private set; }
  11.         private double ecartType;
  12.
  13.         // constructeur
  14.         public TableauDeNotes(string matière, Note[] notes) {
  15.             // mémorisation via les propriétés publiques
  16.             Matière = matière;
  17.             Notes = notes;
  18.             // calcul de la moyenne des notes
  19.             double somme = 0;
  20.             for (int i = 0; i < Notes.Length; i++) {
  21.                 somme += Notes[i].Valeur;
  22.             }
  23.             if (Notes.Length != 0) Moyenne = somme / Notes.Length;
  24.             else Moyenne = -1;
  25.             // écart-type
  26.             double carrés = 0;
  27.             for (int i = 0; i < Notes.Length; i++) {
  28.                 carrés += Math.Pow((Notes[i].Valeur - Moyenne), 2);
  29.             }//for
  30.             if (Notes.Length != 0)
  31.                 ecartType = Math.Sqrt(carrés / Notes.Length);
  32.             else ecartType = -1;
  33.         }//constructeur
  34.
  35.         public double EcartType() {
  36.             return ecartType;
  37.         }
  38.
  39.         // ToString
  40.         public override string ToString() {
  41.             StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
  42.             int i;
  43.             // on concatène toutes les notes
  44.             for (i = 0; i < Notes.Length-1; i++) {
  45. valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("],");
  46.             };
  47.             //dernière note
  48.             if (Notes.Length != 0) {
  49. valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("]");
  50.             }
  51.             valeur.Append(")");
  52.             // fin
  53.             return valeur.ToString();
  54.         }//ToString
  55.
  56.     }//classe
  57. }
  • ligne 6 : la classe TableauDeNotes implémente l'interface IStats . Elle doit donc implémenter la propriété Moyenne et la méthode EcartType . Celles-ci sont implémentées lignes 10 ( Moyenne ) et 35-37 ( EcartType )
  • lignes 8-10 : trois propriétés automatiques
  • ligne 8 : la matière dont l'objet mémorise les notes
  • ligne 9 : le tableau des notes des élèves (Elève, Note)
  • ligne 10 : la moyenne des notes - propriété implémentant la propriété Moyenne de l'interface IStats.
  • ligne 11 : champ mémorisant l'écart-type des notes - la méthode get associée EcartType des lignes 35-37 implémente la méthode EcartType de l'interface IStats .
  • ligne 9 : les notes sont mémorisées dans un tableau. Celui-ci est transmis lors de la construction de la classe TableauDeNotes au constructeur des lignes 14-33.
  • lignes 14-33 : le constructeur. On suppose ici que les notes transmises au constructeur ne bougeront plus par la suite. Aussi utilise-t-on le constructeur pour calculer tout de suite la moyenne et l'écart-type de ces notes et les mémoriser dans les champs des lignes 10-11. La moyenne est mémorisée dans le champ privé sous-jacent à la propriété automatique Moyenne de la ligne 10 et l'écart-type dans le champ privé de la ligne 11.
  • ligne 10 : la méthode get de la propriété automatique Moyenne rendra le champ privé sous-jacent.
  • lignes 35-37 : la méthode EcartType rend la valeur du champ privé de la ligne 11.

Il y a quelques subtilités dans ce code :

  • ligne 23 : la méthode set de la propriété Moyenne est utilisée pour faire l'affectation. Cette méthode a été déclarée privée ligne 10 afin que l'affectation d'une valeur à la propriété Moyenne ne soit possible qu'à l'intérieur de la classe.
  • lignes 40-54 : utilisent un objet StringBuilder pour construire la chaîne représentant l'objet TableauDeNotes afin d'améliorer les performances. On peut noter que la lisibilité du code en pâtit beaucoup. C'est le revers de la médaille.

Dans la classe précédente, les notes étaient enregistrées dans un tableau. Il n'était pas possible d'ajouter une nouvelle note après construction de l'objet TableauDeNotes . Nous proposons maintenant une seconde implémentation de l'interface IStats , appelée ListeDeNotes , où cette fois les notes seraient enregistrées dans une liste, avec possibilité d'ajouter des notes après construction initiale de l'objet ListeDeNotes.

Le code de la classe ListeDeNotes est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Text;
  3. using System.Collections.Generic;
  4.
  5. namespace Chap2 {
  6.
  7.     public class ListeDeNotes : IStats {
  8.         // attributs
  9.         public string Matière { get; set; }
  10.         public List<Note> Notes { get; set; }
  11.         public double moyenne = -1;
  12.         public double ecartType = -1;
  13.
  14.         // constructeur
  15.         public ListeDeNotes(string matière, List<Note> notes) {
  16.             // mémorisation via les propriétés publiques
  17.             Matière = matière;
  18.             Notes = notes;
  19.         }//constructeur
  20.
  21.         // ajout d'une note
  22.         public void Ajouter(Note note) {
  23.             // ajout de la note
  24.             Notes.Add(note);
  25.             // moyenne et écart type réinitialisés
  26.             moyenne = -1;
  27.             ecartType = -1;
  28.         }
  29.
  30.         // ToString
  31.         public override string ToString() {
  32.             StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
  33.             int i;
  34.             // on concatène toutes les notes
  35.             for (i = 0; i < Notes.Count - 1; i++) {
  36. valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("],");
  37.             };
  38.             //dernière note
  39.             if (Notes.Count != 0) {
  40. valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("]");
  41.             }
  42.             valeur.Append(")");
  43.             // fin
  44.             return valeur.ToString();
  45.         }//ToString
  46.
  47.         // moyenne des notes
  48.         public double Moyenne {
  49.             get {
  50.                 if (moyenne != -1) return moyenne;
  51.                 // calcul de la moyenne des notes
  52.                 double somme = 0;
  53.                 for (int i = 0; i < Notes.Count; i++) {
  54.                     somme += Notes[i].Valeur;
  55.                 }
  56.                 // on rend la moyenne
  57.                 if (Notes.Count != 0) moyenne = somme / Notes.Count;
  58.                 return moyenne;
  59.             }
  60.         }
  61.
  62.         public double EcartType() {
  63.             // écart-type
  64.             if (ecartType != -1) return ecartType;
  65.             // moyenne
  66.             double moyenne = Moyenne;
  67.             double carrés = 0;
  68.             for (int i = 0; i < Notes.Count; i++) {
  69.                 carrés += Math.Pow((Notes[i].Valeur - moyenne), 2);
  70.             }//for
  71.             // on rend l'écart type
  72.             if (Notes.Count != 0)
  73.                 ecartType = Math.Sqrt(carrés / Notes.Count);
  74.             return ecartType;
  75.         }
  76.     }//classe
  77. }
  • ligne 7 : la classe ListeDeNotes implémente l'interface IStats
  • ligne 10 : les notes sont mises maintenant dans une liste plutôt qu'un tableau
  • ligne 11 : la propriété automatique Moyenne de la classe TableauDeNotes a été abandonnée ici au profit d'un champ privé moyenne , ligne 11, associé à la propriété publique en lecture seule Moyenne des lignes 48-60
  • lignes 22-28 : on peut désormais ajouter une note à celles déjà mémorisées, ce qu'on ne pouvait pas faire précédemment.
  • lignes 15-19 : du coup, la moyenne et l'écart-type ne sont plus calculés dans le constructeur mais dans les méthodes de l'interface elles-mêmes : Moyenne (lignes 48-60) et EcartType (62-76). Le recalcul n'est cependant relancé que si la moyenne et l'écart-type sont différents de -1 (lignes 50 et 64).

Une classe de test pourrait être la suivante :

Cacher Sélectionnez
  1. using System;
  2. using System.Collections.Generic;
  3.
  4. namespace Chap2 {
  5.     class Program1 {
  6.         static void Main(string[] args) {
  7.             // qqs élèves & notes d'anglais
  8.             Elève[] élèves1 =  { new Elève { Prénom = "Paul", Nom = "Martin" }, new Elève { Prénom = "Maxime", Nom = "Germain" }, new Elève { Prénom = "Berthine", Nom = "Samin" } };
  9.             Note[] notes1 = { new Note { Elève = élèves1[0], Valeur = 14 }, new Note { Elève = élèves1[1], Valeur = 16 }, new Note { Elève = élèves1[2], Valeur = 18 } };
  10.             // qu'on enregistre dans un objet TableauDeNotes
  11.             TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
  12.             // affichage moyenne et écart-type
  13.             Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", anglais.Moyenne, anglais.EcartType(), anglais);
  14.             // on met les élèves et la matière dans un objet ListeDeNotes
  15.             ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
  16.             // affichage moyenne et écart-type
  17.             Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
  18.             // on rajoute une note
  19.             français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
  20.             // affichage moyenne et écart-type
  21.             Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
  22.         }
  23.     }
  24. }
  • ligne 8 : création d'un tableau d'élèves avec utilisation du constructeur sans paramètres et initialisation via les propriétés publiques
  • ligne 9 : création d'un tableau de notes selon la même technique
  • ligne 11 : un objet TableauDeNotes dont on calcule la moyenne et l'écart-type ligne 13
  • ligne 15 : un objet ListeDeNotes dont on calcule la moyenne et l'écart-type ligne 17. La classe List<Note> a un constructeur admettant un objet implémentant l'interface IEnumerable<Note> . Le tableau notes1 implémente cette interface et peut être utilisé pour construire l'objet List<Note> .
  • ligne 19 : ajout d'une nouvelle note
  • ligne 21 : recalcul de la moyenne et écart-type

Les résultats de l'exécution sont les suivants :

Cacher Sélectionnez
  1. matière=anglais, notes=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18]), Moyenne=16, Ecart-type=1,63299316185545
  2. matière=français, notes=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18]), Moyenne=16, Ecart-type=1,63299316185545
  3. matière=français, notes=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18],[Jérôme,Jaric,10]), Moyenne=14,5, Ecart-type=2,95803989154981

Dans l'exemple précédent, deux classes implémentent l'interface IStats . Ceci dit, l'exemple ne fait pas apparaître l'intérêt de l'interface IStats . Réécrivons le programme de test de la façon suivante :

Cacher Sélectionnez
  1. using System;
  2. using System.Collections.Generic;
  3.
  4. namespace Chap2 {
  5.     class Program2 {
  6.         static void Main(string[] args) {
  7.             // qqs élèves & notes d'anglais
  8.             Elève[] élèves1 =  { new Elève { Prénom = "Paul", Nom = "Martin" }, new Elève { Prénom = "Maxime", Nom = "Germain" }, new Elève { Prénom = "Berthine", Nom = "Samin" } };
  9.             Note[] notes1 = { new Note { Elève = élèves1[0], Valeur = 14 }, new Note { Elève = élèves1[1], Valeur = 16 }, new Note { Elève = élèves1[2], Valeur = 18 } };
  10.             // qu'on enregistre dans un objet TableauDeNotes
  11.             TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
  12.             // affichage moyenne et écart-type
  13.             AfficheStats(anglais);
  14.             // on met les élèves et la matière dans un objet ListeDeNotes
  15.             ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
  16.             // affichage moyenne et écart-type
  17.             AfficheStats(français);
  18.             // on rajoute une note
  19.             français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
  20.             // affichage moyenne et écart-type
  21.             AfficheStats(français);
  22.         }
  23.
  24.         // affichage moyenne et écart-type d'un type IStats
  25.         static void AfficheStats(IStats valeurs) {
  26.             Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", valeurs.Moyenne, valeurs.EcartType(), valeurs);
  27.         }
  28.     }
  29. }
  • lignes 25-27 : la méthode statique AfficheStats reçoit pour paramètre un type IStats , donc un type Interface. Cela signifie que le paramètre effectif peut être tout objet implémentant l'interface IStats . Quand on utilise une donnée ayant le type d'une interface, cela signifie qu'on n'utilisera que les méthodes de l'interface implémentées par la donnée. On fait abstraction du reste. On a là une propriété proche du polymorphisme vu pour les classes. Si un ensemble de classes Ci non liées entre-elles par héritage (donc on ne peut utiliser le polymorphisme de l'héritage) présente un ensemble de méthodes de même signature, il peut être intéressant de regrouper ces méthodes dans une interface I qu'implémenteraient toutes les classes concernées. Des instances de ces classes Ci peuvent alors être utilisées comme paramètres effectifs de fonctions admettant un paramètre formel de type I, c.a.d. des fonctions n'utilisant que les méthodes des objets Ci définies dans l'interface I et non les attributs et méthodes particuliers des différentes classes Ci.
  • ligne 13 : la méthode AfficheStats est appelée avec un type TableauDeNotes qui implémente l'interface IStats
  • ligne 17 : idem avec un type ListeDeNotes

Les résultats de l'exécution sont identiques à ceux de la précédente.

Une variable peut être du type d'une interface. Ainsi, on peut écrire :

Cacher Sélectionnez
  1. IStats stats1=new TableauDeNotes(...);
  2. ...
  3. stats1=new ListeDeNotes(...);

La déclaration de la ligne 1 indique que stats1 est l'instance d'une classe implémentant l'interface IStats . Cette déclaration implique que le compilateur ne permettra l'accès dans stats1 qu'aux méthodes de l'interface : la propriété Moyenne et la méthode EcartType .

Notons enfin que l'implémentation d'interfaces peut être multiple, c.a.d. qu'on peut écrire

Cacher Sélectionnez
  1. public class ClasseDérivée:ClasseDeBase,I1,I2,..,In{
  2. ...
  3. }

où les Ij sont des interfaces.

V-G. Les classes abstraites

Une classe abstraite est une classe qu'on ne peut instancier. Il faut créer des classes dérivées qui elles pourront être instanciées.

On peut utiliser des classes abstraites pour factoriser le code d'une lignée de classes. Examinons le cas suivant :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     abstract class Utilisateur {
  5.         // champs
  6.         private string login;
  7.         private string motDePasse;
  8.         private string role;
  9.
  10.         // constructeur
  11.         public Utilisateur(string login, string motDePasse) {
  12.             // on enregistre les informations
  13.             this.login = login;
  14.             this.motDePasse = motDePasse;
  15.             // on identifie l'utilisateur
  16.             role=identifie();
  17.             // identifié ?
  18.             if (role == null) {
  19.                 throw new ExceptionUtilisateurInconnu(String.Format("[{0},{1}]", login, motDePasse));
  20.             }
  21.         }
  22.
  23.         // toString
  24.         public override string ToString() {
  25.             return String.Format("Utilisateur[{0},{1},{2}]", login, motDePasse, role);
  26.         }
  27.
  28.         // identifie
  29.         abstract public string identifie();
  30.     }
  31. }
  • lignes 11-21 : le contructeur de la classe Utilisateur . Cette classe mémorise des informations sur l'utilisateur d'une application web. Celle-ci a divers types d'utilisateurs authentifiés par un login / mot de passe (lignes 6-7). Ces deux informations sont vérifiées auprès d'un service LDAP pour certains utilisateurs, auprès d'un SGBD pour d'autres, etc…
  • lignes 13-14 : les informations d'authentification sont mémorisées
  • ligne 16 : elles sont vérifiées par une méthode identifie. Parce que la méthode d'identification n'est pas connue, elle est déclarée abstraite ligne 29 avec le mot clé abstract. La méthode identifie rend une chaîne de caractères précisant le rôle de l'utilisateur (en gros ce qu'il a le droit de faire). Si cette chaîne est le pointeur null , une exception est lancée ligne 19.
  • ligne 4 : parce qu'elle a une méthode abstraite, la classe elle-même est déclarée abstraite avec le mot clé abstract.
  • ligne 29 : la méthode abstraite identifie n'a pas de définition. Ce sont les classes dérivées qui lui en donneront une.
  • lignes 24-26 : la méthode ToString qui identifie une instance de la classe.

On suppose ici que le développeur veut avoir la maîtrise de la construction des instances de la classe Utilisateur et des classes dérivées, peut-être parce qu'il veut être sûr qu'une exception d'un certain type est lancée si l'utilisateur n'est pas reconnu (ligne 19). Les classes dérivées pourront s'appuyer sur ce constructeur. Elles devront pour cela fournir la méthode identifie.

La classe ExceptionUtilisateurInconnu est la suivante :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class ExceptionUtilisateurInconnu : Exception {
  5.         public ExceptionUtilisateurInconnu(string message) : base(message){
  6.         }
  7.     }
  8. }
  • ligne 3 : elle dérive de la classe Exception
  • lignes 4-6 : elle n'a qu'un unique constructeur qui admet pour paramètre un message d'erreur. Celui-ci est passé à la classe parent (ligne 5) qui a ce même constructeur.

Nous dérivons maintenant la classe Utilisateur dans la classe fille Administrateur :

Cacher Sélectionnez
  1. namespace Chap2 {
  2.     class Administrateur : Utilisateur {
  3.         // constructeur
  4.         public Administrateur(string login, string motDePasse)
  5.             : base(login, motDePasse) {
  6.         }
  7.
  8.         // identifie
  9.         public override string identifie() {
  10.             // identification LDAP
  11.             // ...
  12.             return "admin";
  13.         }
  14.     }
  15. }
  • lignes 4-6 : le constructeur se contente de passer à sa classe parent les paramètres qu'il reçoit
  • lignes 9-12 : la méthode identifie de la classe Administrateur . On suppose qu'un administrateur est identifié par un système LDAP. Cette méthode redéfinit la méthode identifie de sa classe parent. Parce qu'elle redéfinit une méthode abstraite, il est inutile de mettre le mot clé override.

Nous dérivons maintenant la classe Utilisateur dans la classe fille Observateur :

Cacher Sélectionnez
  1. namespace Chap2 {
  2.     class Observateur : Utilisateur{
  3.         // constructeur
  4.         public Observateur(string login, string motDePasse)
  5.             : base(login, motDePasse) {
  6.         }
  7.
  8.         //identifie
  9.         public override string identifie() {
  10.             // identification SGBD
  11.             // ...
  12.             return "observateur";
  13.         }
  14.
  15.     }
  16. }
  • lignes 4-6 : le constructeur se contente de passer à sa classe parent les paramètres qu'il reçoit
  • lignes 9-13 : la méthode identifie de la classe Observateur . On suppose qu'un observateur est identifié par vérification de ses données d'identification dans une base de données.

Au final, les objets Administrateur et Observateur sont instanciés par le même constructeur, celui de la classe parent Utilisateur. Ce constructeur va utiliser la méthode identifie que ces classes fournissent.

Une troisième classe Inconnu dérive également de la classe Utilisateur :

Cacher Sélectionnez
  1. namespace Chap2 {
  2.     class Inconnu : Utilisateur{
  3.
  4.         // constructeur
  5.         public Inconnu(string login, string motDePasse)
  6.             : base(login, motDePasse) {
  7.         }
  8.
  9.         //identifie
  10.         public override string identifie() {
  11.             // utilisateur pas connu
  12.             // ...
  13.             return null;
  14.         }
  15.
  16.     }
  17. }
  • ligne 13 : la méthode identifie rend le pointeur null pour indiquer que l'utilisateur n'a pas été reconnu.

Un programme de test pourrait être le suivant :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program {
  5.         static void Main(string[] args) {
  6.             Console.WriteLine(new Observateur("observer","mdp1"));
  7.             Console.WriteLine(new Administrateur("admin", "mdp2"));
  8.             try {
  9.                 Console.WriteLine(new Inconnu("xx", "yy"));
  10.             } catch (ExceptionUtilisateurInconnu e) {
  11.                 Console.WriteLine("Utilisateur non connu : "+ e.Message);
  12.             }
  13.         }
  14.     }
  15. }

On notera que lignes 6, 7 et 9, c'est la méthode [Utilisateur].ToString() qui sera utilisée par la méthode W riteLine .

Les résultats de l'exécution sont les suivants :

Cacher Sélectionnez
  1. Utilisateur[observer,mdp1,observateur]
  2. Utilisateur[admin,mdp2,admin]
  3. Utilisateur non connu : [xx,yy]

V-H. Les classes, interfaces, méthodes génériques

Supposons qu'on veuille écrire une méthode permutant deux nombres entiers. Cette méthode pourrait être la suivante :

Cacher Sélectionnez
  1.         public static void Echanger1(ref int value1, ref int value2){
  2.             // on échange les références value1 et value2
  3.             int temp = value2;
  4.             value2 = value1;
  5.             value1 = temp;
  6. }

Maintenant, si on voulait permuter deux références sur des objets Personne , on écrirait :

Cacher Sélectionnez
  1.         public static void Echanger2(ref Personne value1, ref Personne value2){
  2.             // on échange les références value1 et value2
  3.             Personne temp = value2;
  4.             value2 = value1;
  5.             value1 = temp;
  6. }

Ce qui différencie les deux méthodes, c'est le type T des paramètres : int dans Echanger1 , Personne dans Echanger2 . Les classes et interfaces génériques répondent au besoin de méthodes qui ne diffèrent que par le type de certains de leurs paramètres.

Avec une classe générique, la méthode Echanger pourrait être réécrite de la façon suivante :

Cacher Sélectionnez
  1. namespace Chap2 {
  2.     class Generic1<T> {
  3.         public static void Echanger(ref T value1, ref T value2){
  4.             // on échange les références value1 et value2
  5.             T temp = value2;
  6.             value2 = value1;
  7.             value1 = temp;
  8.         }
  9.     }
  10. }
  • ligne 2 : la classe Generic1 est paramétrée par un type noté T. On peut lui donner le nom que l'on veut. Ce type T est ensuite réutilisé dans la classe aux lignes 3 et 5. On dit que la classe Generic1 est une classe générique.
  • ligne 3 : définit les deux références sur un type T à permuter
  • ligne 5 : la variable temporaire temp a le type T.

Un programme de test de la classe pourrait être le suivant :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program {
  5.         static void Main(string[] args) {
  6.             // int
  7.             int i1 = 1, i2 = 2;
  8.             Generic1<int>.Echanger(ref i1, ref i2);
  9.             Console.WriteLine("i1={0},i2={1}", i1, i2);
  10.             // string
  11.             string s1 = "s1", s2 = "s2";
  12.             Generic1<string>.Echanger(ref s1, ref s2);
  13.             Console.WriteLine("s1={0},s2={1}", s1, s2);
  14.             // Personne
  15.             Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
  16.             Generic1<Personne>.Echanger(ref p1, ref p2);
  17.             Console.WriteLine("p1={0},p2={1}", p1, p2);
  18.
  19.         }
  20.     }
  21. }
  • ligne 8 : lorsqu'on utilise une classe générique paramétrée par des types T1, T2… ces derniers doivent être "instanciés". Ligne 8, on utilise la méthode statique Echanger du type Generic1<int> pour indiquer que les références passées à la méthode Echanger sont de type int .
  • ligne 12 : on utilise la méthode statique Echanger du type Generic1<string> pour indiquer que les références passées à la méthode Echanger sont de type string .
  • ligne 16 : on utilise la méthode statique Echanger du type Generic1<Personne> pour indiquer que les références passées à la méthode Echanger sont de type Personne .

Les résultats de l'exécution sont les suivants :

Cacher Sélectionnez
  1. i1=2,i2=1
  2. s1=s2,s2=s1
  3. p1=[pauline, dard, 55],p2=[jean, clu, 20]

La méthode Echanger aurait pu également être écrite de la façon suivante :

Cacher Sélectionnez
  1. namespace Chap2 {
  2.     class Generic2 {
  3.         public static void Echanger<T>(ref T value1, ref T value2){
  4.             // on échange les références value1 et value2
  5.             T temp = value2;
  6.             value2 = value1;
  7.             value1 = temp;
  8.         }
  9.     }
  10. }
  • ligne 2 : la classe Generic2 n'est plus générique
  • ligne 3 : la méthode statique Echanger est générique

Le programme de test devient alors le suivant :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program2 {
  5.         static void Main(string[] args) {
  6.             // int
  7.             int i1 = 1, i2 = 2;
  8.             Generic2.Echanger<int>(ref i1, ref i2);
  9.             Console.WriteLine("i1={0},i2={1}", i1, i2);
  10.             // string
  11.             string s1 = "s1", s2 = "s2";
  12.             Generic2.Echanger<string>(ref s1, ref s2);
  13.             Console.WriteLine("s1={0},s2={1}", s1, s2);
  14.             // Personne
  15.             Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
  16.             Generic2.Echanger<Personne>(ref p1, ref p2);
  17.             Console.WriteLine("p1={0},p2={1}", p1, p2);
  18.         }
  19.     }
  20. }
  • lignes 8, 12 et 16 : on appelle la méthode Echanger en précisant dans <> le type des paramètres. En fait, le compilateur est capable de déduire d'après le type des paramètres effectifs, la variante de la méthode Echanger à utiliser. Aussi, l'écriture suivante est-elle légale :
Cacher Sélectionnez
  1.             Generic2.Echanger(ref i1, ref i2);
  2. ...
  3.             Generic2.Echanger(ref s1, ref s2);
  4. ...
  5.             Generic2.Echanger(ref p1, ref p2);

Lignes 1, 3 et 5 : la variante de la méthode Echanger appelée n'est plus précisée. Le compilateur est capable de la déduire de la nature des paramètres effectifs utilisés.

On peut mettre des contraintes sur les paramètres génériques :

Image non disponible

Considérons la nouvelle méthode générique Echanger suivante :

Cacher Sélectionnez
  1. namespace Chap2 {
  2.     class Generic3 {
  3.         public static void Echanger<T>(ref T value1, ref T value2) where T : class {
  4.             // on échange les références value1 et value2
  5.             T temp = value2;
  6.             value2 = value1;
  7.             value1 = temp;
  8.         }
  9.     }
  10. }
  • ligne 3 : on exige que le type T soit une référence (classe, interface)

Considérons le programme de test suivant :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program4 {
  5.         static void Main(string[] args) {
  6.             // int
  7.             int i1 = 1, i2 = 2;
  8.             Generic3.Echanger<int>(ref i1, ref i2);
  9.             Console.WriteLine("i1={0},i2={1}", i1, i2);
  10.             // string
  11.             string s1 = "s1", s2 = "s2";
  12.             Generic3.Echanger(ref s1, ref s2);
  13.             Console.WriteLine("s1={0},s2={1}", s1, s2);
  14.             // Personne
  15.             Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
  16.             Generic3.Echanger(ref p1, ref p2);
  17.             Console.WriteLine("p1={0},p2={1}", p1, p2);
  18.
  19.         }
  20.     }
  21. }

Le compilateur déclare une erreur sur la ligne 8 car le type int n'est pas une classe ou une interface, c'est une structure :

Image non disponible

Considérons la nouvelle méthode générique Echanger suivante :

Cacher Sélectionnez
  1. namespace Chap2 {
  2.     class Generic4 {
  3.         public static void Echanger<T>(ref T element1, ref T element2) where T : Interface1 {
  4.             // on récupère la valeur des 2 éléments
  5.             int value1 = element1.Value();
  6.             int value2 = element2.Value();
  7.             // si 1er élément > 2ième élément, on échange les éléments
  8.             if (value1 > value2) {
  9.                 T temp = element2;
  10.                 element2 = element1;
  11.                 element1 = temp;
  12.             }
  13.         }
  14.     }
  15. }
  • ligne 3 : le type T doit implémenter l'interface Interface1 . Celle-ci a une méthode Value, utilisée lignes 5 et 6, qui donne la valeur de l'objet de type T.
  • lignes 8-12 : les deux références element1 et element2 ne sont échangées que si la valeur de element1 est supérieure à la valeur de element2 .

L'interface Interface1 est la suivante :

Cacher Sélectionnez
  1. namespace Chap2 {
  2.     interface Interface1 {
  3.         int Value();
  4.     }
  5. }

Elle est implémentée par la classe Class1 suivante :

Cacher Sélectionnez
  1. using System;
  2. using System.Threading;
  3.
  4. namespace Chap2 {
  5.     class Class1 : Interface1 {
  6.         // valeur de l'objet
  7.         private int value;
  8.
  9.         // constructeur
  10.         public Class1() {
  11.             // attente 1 ms
  12.             Thread.Sleep(1);
  13.             // valeur aléatoire entre 0 et 99
  14.             value = new Random(DateTime.Now.Millisecond).Next(100);
  15.         }
  16.
  17.         // accesseur champ privé value
  18.         public int Value() {
  19.             return value;
  20.         }
  21.
  22.         // état de l'instance
  23.         public override string ToString() {
  24.             return value.ToString();
  25.         }
  26.     }
  27. }
  • ligne 5 : Class1 implémente l'interface Interface1
  • ligne 7 : la valeur d'une instance de Class1
  • lignes 10-14 : le champ value est initialisé avec une valeur aléatoire entre 0 et 99
  • lignes 18-20 : la méthode Value de l'interface Interface1
  • lignes 23-25 : la méthode ToString de la classe

L'interface Interface1 est également implémentée par la classe Class2 :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Class2 : Interface1 {
  5.         // valeurs de l'objet
  6.         private int value;
  7.         private String s;
  8.
  9.         // constructeur
  10.         public Class2(String s) {
  11.             this.s = s;
  12.             value = s.Length;
  13.         }
  14.
  15.         // accesseur champ privé value
  16.         public int Value() {
  17.             return value;
  18.         }
  19.
  20.         // état de l'instance
  21.         public override string ToString() {
  22.             return s;
  23.         }
  24.     }
  25. }
  • ligne 4 : Class2 implémente l'interface Interface1
  • ligne 6 : la valeur d'une instance de Class2
  • lignes 10-13 : le champ value est initialisé avec la longueur de la chaîne de caractères passée au constructeur
  • lignes 16-18 : la méthode Value de l'interface Interface1
  • lignes 21-22 : la méthode ToString de la classe

Un programme de test pourrait être le suivant :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program5 {
  5.         static void Main(string[] args) {
  6.             // échanger des instances de type Class1
  7.             Class1 c1, c2;
  8.             for (int i = 0; i < 5; i++) {
  9.                 c1 = new Class1();
  10.                 c2 = new Class1();
  11.                 Console.WriteLine("Avant échange --> c1={0},c2={1}", c1, c2);
  12.                 Generic4.Echanger(ref c1, ref c2);
  13.                 Console.WriteLine("Après échange --> c1={0},c2={1}", c1, c2);
  14.             }
  15.             // échanger des instances de type Class2
  16.             Class2 c3, c4;
  17.             c3 = new Class2("xxxxxxxxxxxxxx");
  18.             c4 = new Class2("xx");
  19.             Console.WriteLine("Avant échange --> c3={0},c4={1}", c3, c4);
  20.             Generic4.Echanger(ref c3, ref c4);
  21.             Console.WriteLine("Avant échange --> c3={0},c4={1}", c3, c4);
  22.         }
  23.     }
  24. }
  • lignes 8-14 : des instances de type Class1 sont échangées
  • lignes 16-22 : des instances de type Class2 sont échangées

Les résultats de l'exécution sont les suivants :

Cacher Sélectionnez
  1. Avant échange --> c1=43,c2=79
  2. Après échange --> c1=43,c2=79
  3. Avant échange --> c1=72,c2=56
  4. Après échange --> c1=56,c2=72
  5. Avant échange --> c1=92,c2=75
  6. Après échange --> c1=75,c2=92
  7. Avant échange --> c1=11,c2=47
  8. Après échange --> c1=11,c2=47
  9. Avant échange --> c1=31,c2=67
  10. Après échange --> c1=31,c2=67
  11. Avant échange --> c3=xxxxxxxxxxxxxx,c4=xx
  12. Après échange --> c3=xx,c4=xxxxxxxxxxxxxx

Pour illustrer la notion , nous allons trier un tableau de personnes d'abord sur leurs noms, puis sur leurs ages. La méthode qui nous permet de trier un tableau est la méthode statique Sort de la classe Array :

Image non disponible

On rappelle qu'une méthode statique s'utilise en préfixant la méthode par le nom de la classe et non par celui d'une instance de la classe. La méthode Sort a différentes signatures (elle est surchargée). Nous utiliserons la signature suivante :

Cacher Sélectionnez
  1. public static void Sort<T>(T[] tableau, IComparer<T> comparateur)

Sort une méthode générique où T désigne un type quelconque. La méthode reçoit deux paramètres :

  • T[] tableau : le tableau d'éléments de type T à trier
  • IComparer<T> comparateur : une référence d'objet implémentant l'interface IComparer<T> .

IComparer<T> est une interface générique définie comme suit :

Cacher Sélectionnez
  1. public interface IComparer<T>{
  2.     int Compare(T t1, T t2);
  3. }

L'interface IComparer<T> n'a qu'une unique méthode. La méthode Compare :

  • reçoit en paramètres deux éléments t1 et t2 de type T
  • rend 1 si t1>t2, 0 si t1==t2, -1 si t1<t2. C'est au développeur de donner une signification aux opérateurs <, ==, >. Par exemple, si p1 et p2 sont deux objets Personne , on pourra dire que p1>p2 si le nom de p1 précède le nom de p2 dans l'ordre alphabétique. On aura alors un tri croissant selon le nom des personnes. Si on veut un tri selon l'âge, on dira que p1>p2 si l'âge de p1 est supérieur à l'âge de p2.
  • pour avoir un tri dans l'ordre décroissant, il suffit d'inverser les résultats +1 et -1

Nous en savons assez pour trier un tabeau de personnes. Le programme est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Collections.Generic;
  3.
  4. namespace Chap2 {
  5.     class Program6 {
  6.         static void Main(string[] args) {
  7.             // un tableau de personnes
  8.             Personne[] personnes1 = { new Personne("claude", "pollon", 25), new Personne("valentine", "germain", 35), new Personne("paul", "germain", 32) };
  9.             // affichage
  10.             Affiche("Tableau à trier", personnes1);
  11.             // tri selon le nom
  12.             Array.Sort(personnes1, new CompareNoms());
  13.             // affichage
  14.             Affiche("Tableau après le tri selon les nom et prénom", personnes1);
  15.             // tri selon l'âge
  16.             Array.Sort(personnes1, new CompareAges());
  17.             // affichage
  18.             Affiche("Tableau après le tri selon l'âge", personnes1);
  19.         }
  20.
  21.         static void Affiche(string texte, Personne[] personnes) {
  22.             Console.WriteLine(texte.PadRight(50, '-'));
  23.             foreach (Personne p in personnes) {
  24.                 Console.WriteLine(p);
  25.             }
  26.         }
  27.     }
  28.
  29.     // classe de comparaison des noms et prénoms des personnes
  30.     class CompareNoms : IComparer<Personne> {
  31.         public int Compare(Personne p1, Personne p2) {
  32.             // on compare les noms
  33.             int i = p1.Nom.CompareTo(p2.Nom);
  34.             if (i != 0)
  35.                 return i;
  36.             // égalité des noms - on compare les prénoms
  37.             return p1.Prenom.CompareTo(p2.Prenom);
  38.         }
  39.     }
  40.
  41.     // classe de comparaison des ages des personnes
  42.     class CompareAges : IComparer<Personne> {
  43.         public int Compare(Personne p1, Personne p2) {
  44.             // on compare les ages
  45.             if (p1.Age > p2.Age)
  46.                 return 1;
  47.             else if (p1.Age == p2.Age)
  48.                 return 0;
  49.             else
  50.                 return -1;
  51.         }
  52.     }
  53.
  54. }
  • ligne 8 : le tableau de personnes
  • ligne 12 : le tri du tableau de personnes selon les nom et prénom. Le 2ième paramètre de la méthode générique Sort est une instance d'une classe CompareNoms implémentant l'interface générique IComparer<Personne> .
  • lignes 30-39 : la classe CompareNoms implémentant l'interface générique IComparer<Personne>.
  • lignes 31-38 : implémentation de la méthode générique int CompareTo(T,T) de l'interface IComparer<T> . La méthode utilise la méthode String.CompareTo , présentée page , pour comparer deux chaînes de caractères.
  • ligne 16 : le tri du tableau de personnes selon les âges. Le 2ième paramètre de la méthode générique Sort est une instance d'une classe CompareAges implémentant l'interface générique IComparer<Personne > et définie lignes 42-51.

Les résultats de l'exécution sont les suivants :

Cacher Sélectionnez
  1. Tableau à trier-----------------------------------
  2. [claude, pollon, 25]
  3. [valentine, germain, 35]
  4. [paul, germain, 32]
  5. Tableau après le tri selon les nom et prénom------
  6. [paul, germain, 32]
  7. [valentine, germain, 35]
  8. [claude, pollon, 25]
  9. Tableau après le tri selon l'âge------------------
  10. [claude, pollon, 25]
  11. [paul, germain, 32]
  12. [valentine, germain, 35]

V-I. Les espaces de noms

Pour écrire une ligne à l'écran, nous utilisons l'instruction

Cacher Sélectionnez
  1. Console.WriteLine(...)

Si nous regardons la définition de la classe Console

Cacher Sélectionnez
  1. Namespace: System
  2. Assembly: Mscorlib (in Mscorlib.dll)

on découvre qu'elle fait partie de l'espace de noms System . Cela signifie que la classe Console devrait être désignée par System.Console et on devrait en fait écrire :

Cacher Sélectionnez
  1. System.Console.WriteLine(...)

On évite cela en utilisant une clause using :

Cacher Sélectionnez
  1. using System;
  2. ...
  3. Console.WriteLine(...)

On dit qu'on importe l'espace de noms System avec la clause using . Lorsque le compilateur va rencontrer le nom d'une classe (ici Console ) il va chercher à la trouver dans les différents espaces de noms importés par les clauses using . Ici il trouvera la classe Console dans l'espace de noms System . Notons maintenant la seconde information attachée à la classe Console :

Cacher Sélectionnez
  1. Assembly: Mscorlib (in Mscorlib.dll)

Cette ligne indique dans quelle "assemblage" se trouve la définition de la classe Console . Lorsqu'on compile en-dehors de Visual Studio et qu'on doit donner les références des différentes dll contenant les classes que l'on doit utiliser, cette information peut s'avérer utile. Pour référencer les dll nécessaires à la compilation d'une classe, on écrit :

csc /r:fic1.dll /r:fic2.dll … prog.cs

où csc est le compilateur C#. Lorsqu'on crée une classe, on peut la créer à l'intérieur d'un espace de noms. Le but de ces espaces de noms est d'éviter les conflits de noms entre classes lorsque celles-ci sont vendues par exemple. Considérons deux entreprises E1 et E2 distribuant des classes empaquetées respectivement dans les dll , e 1.dll et e 2.dll . Soit un client C qui achète ces deux ensembles de classes dans lesquelles les deux entreprises ont défini toutes deux une classe Personne . Le client C compile un programme de la façon suivante :

csc /r:e1.dll /r:e2.dll prog.cs

Si le source prog.cs utilise la classe P ersonne , le compilateur ne saura pas s'il doit prendre la classe P ersonne de e 1.dll ou celle de e 2.dll . Il signalera une erreur. Si l'entreprise E1 prend soin de créer ses classes dans un espace de noms appelé E1 et l'entreprise E2 dans un espace de noms appelé E2, les deux classes P ersonne s'appelleront alors E 1.Personne et E2.Personne . Le client devra employer dans ses classes soit E1.Personne , soit E2.Personne mais pas Personne . L'espace de noms permet de lever l'ambiguïté.

Pour créer une classe dans un espace de noms, on écrit :

Cacher Sélectionnez
  1. namespace EspaceDeNoms{
  2.     // définition de la classe
  3. }

V-J. Application exemple - version 2

On reprend le calcul de l'impôt déjà étudié dans le chapitre précédent page et on le traite maintenant en utilisant des classes et des interfaces. Rappelons le problème :

On se propose d'écrire un programme permettant de calculer l'impôt d'un contribuable. On se place dans le cas simplifié d'un contribuable n'ayant que son seul salaire à déclarer (chiffres 2004 pour revenus 2003) :

  • on calcule le nombre de parts du salarié nbParts=nbEnfants/2 +1 s'il n'est pas marié, nbEnfants/2+2 s'il est marié, où nbEnfants est son nombre d'enfants.
  • s'il a au moins trois enfants, il a une demi part de plus
  • on calcule son revenu imposable R=0.72*S où S est son salaire annuel
  • on calcule son coefficient familial QF=R/nbParts
  • on calcule son impôt I. Considérons le tableau suivant :
4262 | 0 | 0
8382 | 0.0683 | 291.09
14753 | 0.1914 | 1322.92
23888 | 0.2826 | 2668.39
38868 | 0.3738 | 4846.98
47932 | 0.4262 | 6883.66
0 | 0.4809 | 9505.54

Chaque ligne a 3 champs. Pour calculer l'impôt I, on recherche la première ligne où QF<=champ1 . Par exemple, si QF=5000 on trouvera la ligne

83820.0683291.09

L'impôt I est alors égal à 0.0683*R - 291.09*nbParts. Si QF est tel que la relation QF<=champ1 n'est jamais vérifiée, alors ce sont les coefficients de la dernière ligne qui sont utilisés. Ici :

00.48099505.54

ce qui donne l'impôt I=0.4809*R - 9505.54*nbParts.

Tout d'abord, nous définissons une structure capable d'encapsuler une ligne du tableau précédent :

Cacher Sélectionnez
  1. namespace Chap2 {
  2.     // une tranche d'impôt
  3.     struct TrancheImpot {
  4.         public decimal Limite { get; set; }
  5.         public decimal CoeffR { get; set; }
  6.         public decimal CoeffN { get; set; }
  7.     }
  8. }

Puis nous définissons une interface IImpot capable de calculer l'impôt :

Cacher Sélectionnez
  1. namespace Chap2 {
  2.     interface IImpot {
  3.         int calculer(bool marié, int nbEnfants, int salaire);
  4.     }
  5. }
  • ligne 3 : la méthode de calcul de l'impôt à partir de trois données : l'état marié ou non du contribuable, son nombre d'enfants, son salaire

Ensuite, nous définissons une classe abstraite implémentant cette interface :

Cacher Sélectionnez
  1. namespace Chap2 {
  2.     abstract class AbstractImpot : IImpot {
  3.
  4.         // les tranches d'impôt nécessaires au calcul de l'impôt
  5.         // proviennent d'une source extérieure
  6.
  7.         protected TrancheImpot[] tranchesImpot;
  8.
  9.         // calcul de l'impôt
  10.         public int calculer(bool marié, int nbEnfants, int salaire) {
  11.             // calcul du nombre de parts
  12.             decimal nbParts;
  13.             if (marié) nbParts = (decimal)nbEnfants / 2 + 2;
  14.             else nbParts = (decimal)nbEnfants / 2 + 1;
  15.             if (nbEnfants >= 3) nbParts += 0.5M;
  16.             // calcul revenu imposable & Quotient familial
  17.             decimal revenu = 0.72M * salaire;
  18.             decimal QF = revenu / nbParts;
  19.             // calcul de l'impôt
  20.             tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
  21.             int i = 0;
  22.             while (QF > tranchesImpot[i].Limite) i++;
  23.             // retour résultat
  24.             return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
  25.         }//calculer
  26.     }//classe
  27.
  28. }
  • ligne 2 : la classe AbstractImpot implémente l'interface IImpot .
  • ligne 7 : les données annuelles du calcul de l'impôt sous forme d'un champ protégé. La classe AbstractImpot ne sait pas comment sera initialisé ce champ. Elle en laisse le soin aux classes dérivées. C'est pourquoi elle est déclarée abstraite (ligne 2) afin d'en interdire toute instanciation.
  • lignes 10-25 : l'implémentation de la méthode calculer de l'interface IImpot . Les classes dérivées n'auront pas à réécrire cette méthode. La classe AbstractImpot sert ainsi de classe de factorisation des classes dérivées. On y met ce qui est commun à toutes les classes dérivées.

Une classe implémentant l'interface IImpot peut être construite en dérivant la classe AbstractImpot . C'est ce que nous faisons maintenant :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class HardwiredImpot : AbstractImpot {
  5.
  6.         // tableaux de données nécessaires au calcul de l'impôt
  7.         decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
  8.         decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
  9.         decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };
  10.
  11.         public HardwiredImpot() {
  12.                 // création du tableau des tranches d'impôt
  13.             tranchesImpot = new TrancheImpot[limites.Length];
  14.                 // remplissage
  15.             for (int i = 0; i < tranchesImpot.Length; i++) {
  16.                 tranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
  17.                 }
  18.         }
  19.     }// classe
  20. }// namespace

La classe HardwiredImpot définit, lignes 7-9, en dur les données nécessaires au calcul de l'impôt. Son constructeur (lignes 11-18) utilise ces données pour initialiser le champ protégé tranchesImpot de la classe mère AbstractImpot .

Un programme de test pourait être le suivant :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class Program {
  5.         static void Main() {
  6.             // programme interactif de calcul d'Impot
  7.             // l'utilisateur tape trois données au clavier : marié nbEnfants salaire
  8.             // le programme affiche alors l'Impot à payer
  9.
  10.             const string syntaxe = "syntaxe : Marié NbEnfants Salaire\n"
  11.                             + "Marié : o pour marié, n pour non marié\n"
  12.                             + "NbEnfants : nombre d'enfants\n"
  13.                             + "Salaire : salaire annuel en F";
  14.
  15.             // création d'un objet IImpot
  16.             IImpot impot = new HardwiredImpot();
  17.
  18.             // boucle infinie
  19.             while (true) {
  20.                 // on demande les paramètres du calcul de l'impôt
  21.                 Console.Write("Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :");
  22.                 string paramètres = Console.ReadLine().Trim();
  23.                 // qq chose à faire ?
  24.                 if (paramètres == null || paramètres == "") break;
  25.                 // vérification du nombre d'arguments dans la ligne saisie
  26.                 string[] args = paramètres.Split(null);
  27.                 int nbParamètres = args.Length;
  28.                 if (nbParamètres != 3) {
  29.                     Console.WriteLine(syntaxe);
  30.                     continue;
  31.                 }//if
  32.                 // vérification de la validité des paramètres
  33.                 // marié
  34.                 string marié = args[0].ToLower();
  35.                 if (marié != "o" && marié != "n") {
  36.                     Console.WriteLine(syntaxe + "\nArgument marié incorrect : tapez o ou n");
  37.                     continue;
  38.                 }//if
  39.                 // nbEnfants
  40.                 int nbEnfants = 0;
  41.                 bool dataOk = false;
  42.                 try {
  43.                     nbEnfants = int.Parse(args[1]);
  44.                     dataOk = nbEnfants >= 0;
  45.                 } catch {
  46.                 }//if
  47.                 // donnée correcte ?
  48.                 if (!dataOk) {
  49.                     Console.WriteLine(syntaxe + "\nArgument NbEnfants incorrect : tapez un entier positif ou nul");
  50.                     continue;
  51.                 }
  52.                 // salaire
  53.                 int salaire = 0;
  54.                 dataOk = false;
  55.                 try {
  56.                     salaire = int.Parse(args[2]);
  57.                     dataOk = salaire >= 0;
  58.                 } catch {
  59.                 }//try-catch
  60.                 // donnée correcte ?
  61.                 if (!dataOk) {
  62.                     Console.WriteLine(syntaxe + "\nArgument salaire incorrect : tapez un entier positif ou nul");
  63.                     continue;
  64.                 }
  65.                 // les paramètres sont corrects - on calcule l'Impot
  66.                 Console.WriteLine("Impot=" + impot.calculer(marié == "o", nbEnfants, salaire) + " euros");
  67.                 // contribuable suivant
  68.             }//while
  69.         }
  70.     }
  71. }

Le programme ci-dessus permet à l'utilisateur de faire des simulations répétées de calcul d'impôt.

  • ligne 16 : création d'un objet impot implémentant l'interface IImpot . Cet objet est obtenu par instanciation d'un type HardwiredImpot , un type qui implémente l'interface IImpot . On notera qu'on n'a pas donné à la variable impot , le type HardwiredImpot mais le type IImpot . En écrivant cela, on indique qu'on ne s'intéresse qu'à la méthode calculer de l'objet impot et pas au reste.
  • lignes 19-68 : la boucle des simulations de calcul de l'impôt
  • ligne 22 : les trois paramètres nécessaires à la méthode calculer sont demandés en une seule ligne tapée au clavier.
  • ligne 26 : la méthode [chaine].Split(null) permet de décomposer [chaine] en mots. Ceux-ci sont stockés dans un tableau args .
  • ligne 66 : appel de la méthode calculer de l'objet impot implémentant l'interface IImpot .

Voici un exemple d'exécution du programme :

Cacher Sélectionnez
  1. Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :q s d
  2. syntaxe : Marié NbEnfants Salaire
  3. Marié : o pour marié, n pour non marié
  4. NbEnfants : nombre d'enfants
  5. Salaire : salaire annuel en euros
  6. Argument marié incorrect : tapez o ou n
  7. Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 d
  8. syntaxe : Marié NbEnfants Salaire
  9. Marié : o pour marié, n pour non marié
  10. NbEnfants : nombre d'enfants
  11. Salaire : salaire annuel en euros
  12. Argument salaire incorrect : tapez un entier positif ou nul
  13. Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :q s d f
  14. syntaxe : Marié NbEnfants Salaire
  15. Marié : o pour marié, n pour non marié
  16. NbEnfants : nombre d'enfants
  17. Salaire : salaire annuel en euros
  18. Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 60000
  19. Impot=4282 euros


CSharp tutoriel 2008

Apprentissage du langage C#

  • VI. Classes .NET d'usage courant
    • VI-A. Chercher de l'aide sur les classes .NET
      • VI-A-1. Help/Contents
      • VI-A-2. Help/Index/Search
    • VI-B. Les chaînes de caractères
      • VI-B-1. La classe System.String
      • VI-B-2. La classe System.Text.StringBuilder
    • VI-C. Les tableaux
    • VI-D. Les collections génériques
      • VI-D-1. La classe générique List<T>
      • VI-D-2. La classe Dictionary<TKey,TValue>
    • VI-E. Les fichiers texte
      • VI-E-1. La classe StreamReader
      • VI-E-2. La classe StreamWriter
    • VI-F. Les fichiers binaires
    • VI-G. Les expressions régulières
      • VI-G-1. Vérifier qu'une chaîne correspond à un modèle donné
      • VI-G-2. Trouver toutes les occurrences d'un modèle dans une chaîne
      • VI-G-3. Récupérer des parties d'un modèle
      • VI-G-4. Un programme d'apprentissage
      • VI-G-5. La méthode Split
    • VI-H. Application exemple - version 3

VI. Classes .NET d'usage courant

Nous présentons ici quelques classes de la plate-forme .NET fréquemment utilisées. Auparavant, nous montrons comment obtenir des renseignements sur les quelques centaines de classes disponibles. Cette aide est indispensable au dévelopeur C# même confirmé. Le niveau de qualité d'une aide (accès facile, organisation compréhensible, pertinence des informations…) peut faire le succès ou l'échec d'un environnement de développement.

VI-A. Chercher de l'aide sur les classes .NET

Nous donnons ici quelques indications pour trouver de l'aide avec Visual Studio.NET

VI-A-1. Help/Contents

Image non disponible
  • en [1], prendre l'option Help/Contents du menu.
  • en [2], prendre l'option Visual C# Express Edition
  • en [3], l'arbre de l'aide sur C#
  • en [4], une autre option utile est .NET Framework qui donne accès à toutes les classes du framework .NET.

Faisons le tour des têtes de chapitre de l'aide C# :

Image non disponible
  • [1] : une vue d'ensemble de C#
  • [2] : une série d'exemples sur certains points de C#
  • [3] : un cours C# - pourrait remplacer avantageusement le présent document… Image non disponible
  • [4] : pour aller dans les détails de C#
  • [5] : utile pour les développeurs C++ ou Java. Permet d'éviter quelques pièges.
  • [6] : lorsque vous cherchez des exemples, vous pouvez commencer par là.
Image non disponible
  • [7] : ce qu'il faut savoir pour créer des interfaces graphiques
  • [8] : pour mieux utiliser l'IDE Visual Studio Express Image non disponible
  • [9] : SQL Server Express 2005 est un SGBD de qualité distribué gratuitement. Nous l'utiliserons dans ce cours.

L'aide C# n'est qu'une partie de ce dont a besoin le développeur. L'autre partie est l'aide sur les centaines de classes du framework .NET qui vont lui faciliter son travail.

Image non disponible
  • [1] : on sélectionne l'aide sur le framework .NET
  • [2] : l'aide se trouve dans la branche . NET Framework SDK
  • [3] : la branche .NET Framework Class Library présente toutes les classes .NET selon l'espace de noms auquel elles appartiennent
  • [4] : l'espace de noms System qui a été le plus souvent utilisé dans les exemples des chapitres précédents
Image non disponible
  • [5] : dans l'espace de noms System , un exemple, ici la structure DateTime
Image non disponible
  • [6] : l'aide sur la structure DateTime

VI-A-2. Help/Index/Search

L'aide fournie par MSDN est immense et on peut ne pas savoir où chercher. On peut alors utiliser l'index de l'aide :

Image non disponible
  • en [1], utiliser l'option [Help/Index] si la fenêtre d'aide n'est pas déjà ouverte, sinon utiliser [2] dans une fenêtre d'aide existante.
  • en [3], préciser le domaine dans lequel doit se faire la recherche
  • en [4], préciser ce que vous cherchez, ici une classe
  • en [5], la réponse

Une autre façon de chercher de l'aide est d'utiliser la fonction search de l'aide :

Image non disponible
  • en [1], utiliser l'option [Help/Search] si la fenêtre d'aide n'est pas déjà ouverte, sinon utiliser [2] dans une fenêtre d'aide existante.
  • en [3], préciser ce qui est cherché
  • en [4], filtrer les domaines de recherche
Image non disponible
  • en [5], la réponse sous forme de différents thèmes où le texte cherché a été trouvé.

VI-B. Les chaînes de caractères

VI-B-1. La classe System.String

Image non disponible

La classe System.String est identique au type simple string. Elle présente de nombreuses propriétés et méthodes. En voici quelques-unes :

public int Length { get ; } | nombre de caractères de la chaîne
public bool EndsWith( string value) | rend vrai si la chaîne se termine par value
public bool StartsWith( string value) | rend vrai si la chaîne commence par value
public virtual bool Equals(object obj) | rend vrai si la chaînes est égale à obj - équivalent chaîne==obj
public int IndexOf( string value, int startIndex) | rend la première position dans la chaîne de la chaîne value - la recherche commence à partir du caractère n° startIndex
public int IndexOf( char value, int startIndex) | idem mais pour le caractère value
public string Insert( int startIndex, string value) | insère la chaîne value dans chaîne en position startIndex
public static string Join(string separator, string[] value) | méthode de classe - rend une chaîne de caractères, résultat de la concaténation des valeurs du tableau value avec le séparateur separator
public int LastIndexOf( char value, int startIndex, int count)
public int LastIndexOf( string value, int startIndex, int count) 		idem indexOf mais rend la dernière position au lieu de la première
public string Replace( char oldChar, char newChar) | rend une chaîne copie de la chaîne courante où le caractère oldChar a été remplacé par le caractère newChar
public string [] Split( char [] separator) | la chaîne est vue comme une suite de champs séparés par les caractères présents dans le tableau separator . Le résultat est le tableau de ces champs
public string Substring( int startIndex, int length) | sous-chaîne de la chaîne courante commençant à la position startIndex et ayant length caractères
public string ToLower() | rend la chaîne courante en minuscules
public string ToUpper() | rend la chaîne courante en majuscules
public string Trim() | rend la chaîne courante débarrassée de ses espaces de début et fin

On notera un point important : lorsqu'une méthode rend une chaîne de caractères, celle-ci est une chaîne différente de la chaîne sur laquelle a été appliquée la méthode. Ainsi S1.Trim() rend une chaîne S2 , et S1 et S2 sont deux chaînes différentes.

Une chaîne C peut être considérée comme un tableau de caractères. Ainsi

  • C[i] est le caractère i de C
  • C.Length est le nombre de caractères de C

Considérons l'exemple suivant :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap3 {
  4.     class Program {
  5.         static void Main(string[] args) {
  6.             string uneChaine = "l'oiseau vole au-dessus des nuages";
  7.             affiche("uneChaine=" + uneChaine);
  8.             affiche("uneChaine.Length=" + uneChaine.Length);
  9.             affiche("chaine[10]=" + uneChaine[10]);
  10.             affiche("uneChaine.IndexOf(\"vole\")=" + uneChaine.IndexOf("vole"));
  11.             affiche("uneChaine.IndexOf(\"x\")=" + uneChaine.IndexOf("x"));
  12.             affiche("uneChaine.LastIndexOf('a')=" + uneChaine.LastIndexOf('a'));
  13.             affiche("uneChaine.LastIndexOf('x')=" + uneChaine.LastIndexOf('x'));
  14.             affiche("uneChaine.Substring(4,7)=" + uneChaine.Substring(4, 7));
  15.             affiche("uneChaine.ToUpper()=" + uneChaine.ToUpper());
  16.             affiche("uneChaine.ToLower()=" + uneChaine.ToLower());
  17.             affiche("uneChaine.Replace('a','A')=" + uneChaine.Replace('a', 'A'));
  18.             string[] champs = uneChaine.Split(null);
  19.             for (int i = 0; i < champs.Length; i++) {
  20.                 affiche("champs[" + i + "]=[" + champs[i] + "]");
  21.             }//for
  22.             affiche("Join(\":\",champs)=" + System.String.Join(":", champs));
  23.             affiche("(\"  abc  \").Trim()=[" + "  abc  ".Trim() + "]");
  24.         }//Main
  25.
  26.         public static void affiche(string msg) {
  27.             // affiche msg
  28.             Console.WriteLine(msg);
  29.         }//affiche
  30.     }//classe
  31. }//namespace

L'exécution donne les résultats suivants :

Cacher Sélectionnez
  1. uneChaine=l'oiseau vole au-dessus des nuages
  2. uneChaine.Length=34
  3. chaine[10]=o
  4. uneChaine.IndexOf("vole")=9
  5. uneChaine.IndexOf("x")=-1
  6. uneChaine.LastIndexOf('a')=30
  7. uneChaine.LastIndexOf('x')=-1
  8. uneChaine.Substring(4,7)=seau vo
  9. uneChaine.ToUpper()=L'OISEAU VOLE AU-DESSUS DES NUAGES
  10. uneChaine.ToLower()=l'oiseau vole au-dessus des nuages
  11. uneChaine.Replace('a','A')=l'oiseAu vole Au-dessus des nuAges
  12. champs[0]=[l'oiseau]
  13. champs[1]=[vole]
  14. champs[2]=[au-dessus]
  15. champs[3]=[des]
  16. champs[4]=[nuages]
  17. Join(":",champs)=l'oiseau:vole:au-dessus:des:nuages
  18. ("  abc  ").Trim()=[abc]

Considérons un nouvel exemple :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap3 {
  4.     class Program {
  5.         static void Main(string[] args) {
  6.             // la ligne à analyser
  7.             string ligne = "un:deux::trois:";
  8.             // les séparateurs de champs
  9.             char[] séparateurs = new char[] { ':' };
  10.             // split
  11.             string[] champs = ligne.Split(séparateurs);
  12.             for (int i = 0; i < champs.Length; i++) {
  13.                 Console.WriteLine("Champs[" + i + "]=" + champs[i]);
  14.             }
  15.             // join
  16.             Console.WriteLine("join=[" + System.String.Join(":", champs) + "]");
  17.         }
  18.     }
  19. }

et les résultats d'exécution :

Cacher Sélectionnez
  1. Champs[0]=un
  2. Champs[1]=deux
  3. Champs[2]=
  4. Champs[3]=trois
  5. Champs[4]=
  6. join=[un:deux::trois:]

La méthode Split de la classe String permet de mettre dans un tableau des éléments d'une chaîne de caractères. La définition de la méthode Split utilisée ici est la suivante :

Cacher Sélectionnez
  1.     public string[] Split(char[] separator);
separator | tableau de caractères. Ces caractères représentent les caractères utilisés pour séparer les champs de la chaîne de caractères. Ainsi si la chaîne est " champ1 , champ2 , champ3" on pourra utiliser separator=new char[] {','} . Si le séparateur est une suite d'espaces on utilisera separator=null .
résultat | tableau de chaînes de caractères où chaque élément du tableau est un champ de la chaîne.

La méthode Join est une méthode statique de la classe String :

Cacher Sélectionnez
  1.     public static string Join(string separator, string[] value);
value | tableau de chaînes de caractères
separator | une chaîne de caractères qui servira de séparateur de champs
résultat | une chaîne de caractères formée de la concaténation des éléments du tableau value séparés par la chaîne separator .

VI-B-2. La classe System.Text.StringBuilder

Image non disponible

Précédemment, nous avons dit que les méthodes de la classe String qui s'appliquaient à une chaîne de caractères S1 rendait une autre chaîne S2 . La classe System.Text.StringBuilder permet de manipuler S1 sans avoir à créer une chaîne S2. Cela améliore les performances en évitant la multiplication de chaînes à durée de vie très limitée.

La classe admet divers constructeurs :

StringBuilder() | constructeur par défaut
StringBuilder(String value) | construction et initialisation avec value
StringBuilder(String value, int capacité) | construction et initialisation avec value avec une taille de bloc de capacité caractères.

Un objet StringBuilder travaille avec des blocs de caractères pour stocker la chaîne sous-jacente. Par défaut capacité vaut 16. Le 3ième constructeur ci-dessus permet de préciser la capacité des blocs. Le nombre de blocs de capacité caractères nécessaire pour stocker une chaîne S est ajusté automatiquement par la classe StringBuilder . Il existe des constructeurs pour fixer le nombre maximal de caractères dans un objet StringBuilder . Par défaut, cette capacité maximale est 2 147 483 647.

Voici un exemple illustrant cette notion de capacité :

Cacher Sélectionnez
  1. using System.Text;
  2. using System;
  3. namespace Chap3 {
  4.     class Program {
  5.         static void Main(string[] args) {
  6.             // str
  7.             StringBuilder str = new StringBuilder("test");
  8.             Console.WriteLine("taille={0}, capacité={1}", str.Length, str.Capacity);
  9.             for (int i = 0; i < 10; i++) {
  10.                 str.Append("test");
  11.                 Console.WriteLine("taille={0}, capacité={1}", str.Length, str.Capacity);
  12.             }
  13.             // str2
  14.             StringBuilder str2 = new StringBuilder("test",10);
  15.             Console.WriteLine("taille={0}, capacité={1}", str2.Length, str2.Capacity);
  16.             for (int i = 0; i < 10; i++) {
  17.                 str2.Append("test");
  18.                 Console.WriteLine("taille={0}, capacité={1}", str2.Length, str2.Capacity);
  19.             }
  20.         }
  21.     }
  22. }
  • ligne 7 : création d'un objet StringBuilder avec une taille de bloc de 16 caractères
  • ligne 8 : str.Length est le nombre actuel de caractères de la chaîne str . str.Capacity est le nombre de caractères que peut stocker la chaîne str actuelle avant réallocation d'un nouveau bloc.
  • ligne 10 : str.Append(String S) permet de concaténer la chaîne S de type String à la chaîne str de type StringBuilder .
  • ligne 14 : création d'un objet StringBuilder avec une capacité de bloc de 10 caractères

Le résultat de l'exécution :

Cacher Sélectionnez
  1. taille=4, capacité=16
  2. taille=8, capacité=16
  3. taille=12, capacité=16
  4. taille=16, capacité=16
  5. taille=20, capacité=32
  6. taille=24, capacité=32
  7. taille=28, capacité=32
  8. taille=32, capacité=32
  9. taille=36, capacité=64
  10. taille=40, capacité=64
  11. taille=44, capacité=64
  12. taille=4, capacité=10
  13. taille=8, capacité=10
  14. taille=12, capacité=20
  15. taille=16, capacité=20
  16. taille=20, capacité=20
  17. taille=24, capacité=40
  18. taille=28, capacité=40
  19. taille=32, capacité=40
  20. taille=36, capacité=40
  21. taille=40, capacité=40
  22. taille=44, capacité=80

Ces résultats montrent que la classe suit un algorithme qui lui est propre pour allouer de nouveaux blocs lorsque sa capacité est insuffisante :

  • lignes 4-5 : augmentation de la capacité de 16 caractères
  • lignes 8-9 : augmentation de la capacité de 32 caractères alors que 16 auraient suffi.

Voici quelques-unes des méthodes de la classe :

public StringBuilder Append(string value) | ajoute la chaîne value à l'objet StringBuilder . Rend l'objet StringBuilder . Cette méthode est surchargée pour admettre différents types pour value : byte, int, float, double, decimal…
public StringBuilder Insert(int index, string value) | insère value à la position index . Cette méthode est surchargée comme la précédente pour accepter différents types pour value .
public StringBuilder Remove(int index, int length) | supprime length caractères à partir de la position index .
public StringBuilder Replace(string oldValue, string newValue) | remplace dans StringBuilder , la chaîne oldValue par la chaîne newValue . Il existe une version surchargée (char oldChar, char newChar).
public String ToString() | convertit l'objet S tringBuilder en un objet de type S tring .

Voici un exemple :

Cacher Sélectionnez
  1. using System.Text;
  2. using System;
  3. namespace Chap3 {
  4.     class Program {
  5.         static void Main(string[] args) {
  6.             // str3
  7.             StringBuilder str3 = new StringBuilder("test");
  8.             Console.WriteLine(str3.Append("abCD").Insert(2, "xyZT").Remove(0, 2).Replace("xy", "XY"));
  9.         }
  10.     }
  11. }

et ses résultats :

Cacher Sélectionnez
  1. XYZTstabCD

VI-C. Les tableaux

Les tableaux dérivent de la classe Array :

Image non disponible

La classe Array possède diverses méthodes pour trier un tableau, rechercher un élément dans un tableau, redimensionner un tableau… Nous présentons certaines propriétés et méthodes de cette classe. Elles sont quasiment toutes surchargées, c.a.d. qu'elles existent en différentes variantes. Tout tableau en hérite.

Propriétés

public int Length {get;} | nombre total d'éléments du tableau, quelque soit son nombre de dimensions
public int Rank {get;} | nombre total de dimensions du tableau

Méthodes

public static int BinarySearch<T>(T[] tableau,T value) | rend la position de value dans tableau .
public static int BinarySearch<T>(T[] tableau,int index, int length, T value) | idem mais cherche dans tableau à partir de la position index et sur length éléments
public static void Clear(Array tableau, int index, int length) | met les length éléments de tableau commençant au n° index à 0 si numériques, false si booléens, null si références
public static void Copy(Array source, Array destination, int length) | copie length éléments de source dans destination
public int GetLength(int i) | nombre d'éléments de la dimension n° i du tableau
public int GetLowerBound(int i) | indice du 1er élément de la dimension n° i
public int GetUpperBound(int i) | indice du dernier élément de la dimension n° i
public static int IndexOf<T>(T[] tableau, T valeur) | rend la position de valeur dans tableau ou -1 si valeur n'est pas trouvée.
public static void Resize<T>(ref T[] tableau, int n) | redimensionne tableau à n éléments. Les éléments déjà présents sont conservés.
public static void Sort<T>(T[] tableau, IComparer<T> comparateur) | trie tableau selon un ordre défini par comparateur . Cette méthode a été présentée page .

Le programme suivant illustre l'utilisation de certaines méthodes de la classe Array :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap3 {
  4.     class Program {
  5.         // type de recherche
  6.         enum TypeRecherche { linéaire, dichotomique };
  7.
  8.         // méthode principale
  9.         static void Main(string[] args) {
  10.             // lecture des éléments d'un tableau tapés au clavier
  11.             double[] éléments;
  12.             Saisie(out éléments);
  13.             // affichage tableau non trié
  14.             Affiche("Tableau non trié", éléments);
  15.             // Recherche linéaire dans le tableau non trié
  16.             Recherche(éléments, TypeRecherche.linéaire);
  17.             // tri du tableau
  18.             Array.Sort(éléments);
  19.             // affichage tableau trié
  20.             Affiche("Tableau trié", éléments);
  21.             // Recherche dichotomique dans le tableau trié
  22.             Recherche(éléments, TypeRecherche.dichotomique);
  23.         }
  24.
  25.         // saisie des valeurs du tableau éléments
  26.         // éléments : référence sur tableau créé par la méthode
  27.         static void Saisie(out double[] éléments) {
  28.             bool terminé = false;
  29.             string réponse;
  30.             bool erreur;
  31.             double élément = 0;
  32.             int i = 0;
  33.             // au départ, le tableau n'existe pas
  34.             éléments = null;
  35.             // boucle de saisie des éléments du tableau
  36.             while (!terminé) {
  37.                 // question
  38.                 Console.Write("Elément (réel) " + i + " du tableau (rien pour terminer) : ");
  39.                 // lecture de la réponse
  40.                 réponse = Console.ReadLine().Trim();
  41.                 // fin de saisie si chaîne vide
  42.                 if (réponse.Equals(""))
  43.                     break;
  44.                 // vérification saisie
  45.                 try {
  46.                     élément = Double.Parse(réponse);
  47.                     erreur = false;
  48.                 } catch {
  49.                     Console.Error.WriteLine("Saisie incorrecte, recommencez");
  50.                     erreur = true;
  51.                 }//try-catch
  52.                 // si pas d'erreur
  53.                 if (!erreur) {
  54.                     // un élément de plus dans le tableau
  55.                     i += 1;
  56.                     // redimensionnement tableau pour accueillir le nouvel élément
  57.                     Array.Resize(ref éléments, i);
  58.                     // insertion nouvel élément
  59.                     éléments[i - 1] = élément;
  60.                 }
  61.             }//while
  62.         }
  63.
  64.         // méthode générique pour afficher les éléments d'un tableau
  65.         static void Affiche<T>(string texte, T[] éléments) {
  66.             Console.WriteLine(texte.PadRight(50, '-'));
  67.             foreach (T élément in éléments) {
  68.                 Console.WriteLine(élément);
  69.             }
  70.         }
  71.
  72.         // recherche d'un élément dans le tableau
  73.         // éléments : tableau de réels
  74.         // TypeRecherche : dichotomique ou linéaire
  75.         static void Recherche(double[] éléments, TypeRecherche type) {
  76.             // Recherche
  77.             bool terminé = false;
  78.             string réponse = null;
  79.             double élément = 0;
  80.             bool erreur = false;
  81.             int i = 0;
  82.             while (!terminé) {
  83.                 // question
  84.                 Console.WriteLine("Elément cherché (rien pour arrêter) : ");
  85.                 // lecture-vérification réponse
  86.                 réponse = Console.ReadLine().Trim();
  87.                 // fini ?
  88.                 if (réponse.Equals(""))
  89.                     break;
  90.                 // vérification
  91.                 try {
  92.                     élément = Double.Parse(réponse);
  93.                     erreur = false;
  94.                 } catch {
  95.                     Console.WriteLine("Erreur, recommencez...");
  96.                     erreur = true;
  97.                 }//try-catch
  98.                 // si pas d'erreur
  99.                 if (!erreur) {
  100.                     // on cherche l'élément dans le tableau
  101.                     if (type == TypeRecherche.dichotomique)
  102.                         // recherche dichotomique
  103.                         i = Array.BinarySearch(éléments, élément);
  104.                     else
  105.                         // recherche linéaire
  106.                         i = Array.IndexOf(éléments, élément);
  107.                     // Affichage réponse
  108.                     if (i >= 0)
  109.                         Console.WriteLine("Trouvé en position " + i);
  110.                     else
  111.                         Console.WriteLine("Pas dans le tableau");
  112.                 }//if
  113.             }//while
  114.         }
  115.     }
  116. }
  • lignes 27-62 : la méthode Saisie saisit les éléments d'un tableau éléments tapés au clavier. Comme on ne peut dimensionner le tableau à priori (on ne connaît pas sa taille finale), on est obligés de le redimensionner à chaque nouvel élément (ligne 57). Un algorithme plus efficace aurait été d'allouer de la place au tableau par groupe de N éléments. Un tableau n'est cependant pas fait pour être redimensionné . Ce cas là est mieux traité avec une liste (ArrayList, List<T>).
  • lignes 75-113 : la méthode Recherche permet de rechercher dans le tabeau éléments , un élément tapé au clavier. Le mode de recherche est différent selon que le tableau est trié ou non. Pour un tableau non trié, on fait une recherche linéaire avec la méthode IndexOf de la ligne 106. Pour un tableau trié, on fait une recherche dichotomique avec la méthode BinarySearch de la ligne 103.
  • ligne 18 : on trie le tableau éléments . On utilise ici, une variante de Sort qui n'a qu'un paramètre : le tableau à trier. La relation d'ordre utilisée pour comparer les éléments du tableau est alors celle implicite de ces éléments. Ici, les éléments sont numériques. C'est l'ordre naturel des nombres qui est utilisé.

Les résultats écran sont les suivants :

Cacher Sélectionnez
  1. Elément (réel) 0 du tableau (rien pour terminer) : 3,6
  2. Elément (réel) 1 du tableau (rien pour terminer) : 7,4
  3. Elément (réel) 2 du tableau (rien pour terminer) : -1,5
  4. Elément (réel) 3 du tableau (rien pour terminer) : -7
  5. Elément (réel) 4 du tableau (rien pour terminer) :
  6. Tableau non trié----------------------------------
  7. 3,6
  8. 7,4
  9. -1,5
  10. -7
  11. Elément cherché (rien pour arrêter) :
  12. 7,4
  13. Trouvé en position 1
  14. Elément cherché (rien pour arrêter) :
  15. 0
  16. Pas dans le tableau
  17. Elément cherché (rien pour arrêter) :
  18.
  19. Tableau trié--------------------------------------
  20. -7
  21. -1,5
  22. 3,6
  23. 7,4
  24. Elément cherché (rien pour arrêter) :
  25. 7,4
  26. Trouvé en position 3
  27. Elément cherché (rien pour arrêter) :
  28. 0
  29. Pas dans le tableau
  30. Elément cherché (rien pour arrêter) :

VI-D. Les collections génériques

Outre le tableau, il existe diverses classes pour stocker des collections d'éléments. Il existe des versions génériques dans l'espace de noms System.Collections.Generic et des versions non génériques dans System.Collections . Nous présenterons deux collections génériques fréquemment utilisées : la liste et le dictionnaire.

La liste des collections génériques est la suivante :

Image non disponible

VI-D-1. La classe générique List<T>

La classe System.Collections.Generic.List<T> permet d'implémenter des collections d'objets de type T dont la taille varie au cours de l'exécution du programme. Un objet de type List<T> se manipule presque comme un tableau. Ainsi l'élément i d'une liste l est-il noté l[i].

Il existe également un type de liste non générique : ArrayList capable de stocker des références sur des objets quelconques. ArrayList est fonctionnellement équivalente à List<Object>. Un objet ArrayList ressemble à ceci :

Image non disponible

Ci-dessus, les éléments 0, 1 et i de la liste pointent sur des objets de types différents. Il faut qu'un objet soit d'abord créé avant d'ajouter sa référence à la liste ArrayList . Bien qu'un ArrayList stocke des références d'objet, il est possible d'y stocker des nombres. Cela se fait par un mécanisme appelé Boxing : le nombre est encapsulé dans un objet O de type Object et c'est la référence O qui est stocké dans la liste. C'est un mécanisme transparent pour le développeur. On peut ainsi écrire :

Cacher Sélectionnez
  1. ArrayList liste=new ArrayList();
  2. liste.Add(4);

Cela produira le résultat suivant :

Image non disponible

Ci-dessus, le nombre 4 a été encapsulé dans un objet O et la référence O est mémorisée dans la liste. Pour le récupérer, on pourra écrire :

Cacher Sélectionnez
  1.             int i = (int)liste[0];

L'opération Object -> int est appelée Unboxing . Si une liste est entièrement composée de types int , la déclarer comme List<int> améliore les performances. En effet, les nombres de type int sont alors stockés dans la liste elle-même et non dans des types Object extérieurs à la liste. Les opérations Boxing / Unboxing n'ont plus lieu.

Image non disponible

Pour un objet List<T> ou T est une classe, la liste stocke là encore les références des objets de type T :

Image non disponible

Voici quelques-unes des propriétés et méthodes des listes génériques :

Propriétés

public int Count {get;} | nombre d'éléments de la liste
public int Capacity {get;} | nombre d'éléments que la liste peut contenir avant d'être redimensionnée. Ce redimensionnement se fait automatiquement. Cette notion de capacité de liste est analogue à celle de capacité décrite pour la classe StringBuilder page .

Méthodes

public void Add(T item) | ajoute item à la liste
public int BinarySearch<T>(T item) | rend la position de item dans la liste s'il s'y trouve sinon un nombre <0
public int BinarySearch<T>(T item, IComparer<T> comparateur) | idem mais le 2ième paramètre permet de comparer deux éléments de la liste. L'interface IComparer<T> a été présentée page .
public void Clear() | supprime tous les éléments de la liste
public bool Contains(T item) | rend True si item est dans la liste, False sinon
public void CopyTo(T[] tableau) | copie les éléments de la liste dans tableau .
public int IndexOf(T item) | rend la position de item dans tableau ou -1 si valeur n'est pas trouvée.
public void Insert(T item, int index) | insère item à la position index de la liste
public bool Remove(T item) | supprime item de la liste. Rend True si l'opération réussit, False sinon.
public void RemoveAt(int index) | supprime l'élément n° index de la liste
public void Sort(IComparer<T> comparateur) | trie la liste selon un ordre défini par comparateur . Cette méthode a été présentée page .
public void Sort() | trie la liste selon l'ordre défini par le type des éléments de la liste
public T[] ToArray() | rend les éléments de la liste sous forme de tableau

Reprenons l'exemple traité précédemment avec un objet de type Array et traitons-le maintenant avec un objet de type List<T>. Parce que la liste est un objet proche du tableau, le code change peu. Nous ne présentons que les modifications notables :

Cacher Sélectionnez
  1. using System;
  2. using System.Collections.Generic;
  3.
  4. namespace Chap3 {
  5.     class Program {
  6.         // type de recherche
  7.         enum TypeRecherche { linéaire, dichotomique };
  8.
  9.         // méthode principale
  10.         static void Main(string[] args) {
  11.             // lecture des éléments d'une liste tapés au clavier
  12.             List<double> éléments;
  13.             Saisie(out éléments);
  14.             // nombre d'éléments
  15.             Console.WriteLine("La liste a {0} éléments et une capacité de {1} éléments", éléments.Count, éléments.Capacity);
  16.             // affichage liste non triée
  17.             Affiche("Liste non triée", éléments);
  18.             // Recherche linéaire dans la liste non triée
  19.             Recherche(éléments, TypeRecherche.linéaire);
  20.             // tri de la liste
  21.             éléments.Sort();
  22.             // affichage liste triée
  23.             Affiche("Liste triée", éléments);
  24.             // Recherche dichotomique dans la liste triée
  25.             Recherche(éléments, TypeRecherche.dichotomique);
  26.         }
  27.
  28.         // saisie des valeurs de la liste éléments
  29.         // éléments : référence sur la liste créée par la méthode
  30.         static void Saisie(out List<double> éléments) {
  31. ...
  32.             // au départ, la liste est vide
  33.             éléments = new List<double>();
  34.             // boucle de saisie des éléments de la liste
  35.             while (!terminé) {
  36. ...
  37.                 // si pas d'erreur
  38.                 if (!erreur) {
  39.                     // un élément de plus dans la liste
  40.                     éléments.Add(élément);
  41.                 }
  42.             }//while
  43.         }
  44.
  45.         // méthode générique pour afficher les éléments d'un objet énumérable
  46.         static void Affiche<T>(string texte, IEnumerable<T> éléments) {
  47.             Console.WriteLine(texte.PadRight(50, '-'));
  48.             foreach (T élément in éléments) {
  49.                 Console.WriteLine(élément);
  50.             }
  51.         }
  52.
  53.         // recherche d'un élément dans la liste
  54.         // éléments : liste de réels
  55.         // TypeRecherche : dichotomique ou linéaire
  56.         static void Recherche(List<double> éléments, TypeRecherche type) {
  57. ...
  58.             while (!terminé) {
  59. ...
  60.                 // si pas d'erreur
  61.                 if (!erreur) {
  62.                     // on cherche l'élément dans la liste
  63.                     if (type == TypeRecherche.dichotomique)
  64.                         // recherche dichotomique
  65.                         i = éléments.BinarySearch(élément);
  66.                     else
  67.                         // recherche linéaire
  68.                         i = éléments.IndexOf(élément);
  69.                     // Affichage réponse
  70. ...
  71.                 }//if
  72.             }//while
  73.         }
  74.     }
  75. }
  • lignes 46-51 : la méthode générique Affiche<T> admet deux paramètres :
  • le 1er paramètre est un texte à écrire
  • le 2ième paramètre est un objet implémentant l'interface générique IEnumerable<T> :
Cacher Sélectionnez
  1. public interface IEnumerable<T>{
  2.     IEnumerator GetEnumerator();
  3.     IEnumerator<T> GetEnumerator();
  4. }

La structure foreach( T élément in éléments) de la ligne 48, est valide pour tout objet éléments implémentant l'interface IEnumerable . Les tableaux ( Array ) et les listes ( List<T> ) implémentent l'interface IEnumerable<T> . Aussi la méthode Affiche convient-elle aussi bien pour afficher des tableaux que des listes.

Les résultats d'exécution du programme sont les mêmes que dans l'exemple utilisant la classe Array .

VI-D-2. La classe Dictionary<TKey,TValue>

La classe System.Collections.Generic.Dictionary<TKey,TValue> permet d'implémenter un dictionnaire. On peut voir un dictionnaire comme un tableau à deux colonnes :

clé | valeur
clé1 | valeur1
clé2 | valeur2
.. | …

Dans la classe Dictionary<TKey,TValue> les clés sont de type Tkey , les valeurs de type TValue . Les clés sont uniques, c.a.d. qu'il ne peut y avoir deux clés identiques. Un tel dictionnaire pourrait ressembler à ceci si les types TKey et TValue désignaient des classes :

Image non disponible

La valeur associée à la clé C d'un dictionnaire D est obtenue par la notation D[C]. Cette valeur est en lecture et écriture. Ainsi on peut écrire :

Cacher Sélectionnez
  1. TValue v=...;
  2. TKey c=...;
  3. Dictionary<TKey,TValue> D=new Dictionary<TKey,TValue>();
  4. D[c]=v;
  5. v=D[c];

Si la clé c n'existe pas dans le dictionnaire D , la notation D[c] lance une exception.

Les méthodes et propriétés principales de la classe Dictionary<TKey,TValue> sont les suivantes :

Constructeurs

public Dictionary<TKey,TValue>() constructeur sans paramètres - construit un dictionnaire vide. Il existe plusieurs autres constructeurs.

Propriétés

public int Count {get;} | nombre d'entrées (clé, valeur) dans le dictionnaire
public Dictionary<TKey,TValue>.KeyCollection Keys {get;} | collection des clés du dictionnaire.
public Dictionary<TKey,TValue>.ValueCollection Values {get;} | collection des valeurs du dictionnaire.

Méthodes

public void Add(TKey key, TValue value) | ajoute le couple (key, value) au dictionnaire
public void Clear() | supprime tous les couples du dictionnaire
public bool ContainsKey (TKey key) | rend True si key est une clé du dictionnaire, False sinon
public bool ContainsValue (TValue value) | rend True si value est une valeur du dictionnaire, False sinon
public void CopyTo(T[] tableau) | copie les éléments de la liste dans tableau .
public bool Remove(TKey key) | supprime du dictionnaire le couple de clé key. Rend True si l'opération réussit, False sinon.
public bool TryGetValue(TKey key,out TValue value) | rend dans value, la valeur associée à la clé key si cette dernière existe, sinon rend la valeur par défaut du type TValue (0 pour les nombres, false pour les booléens, null pour les références d'objet)

Considérons le programme exemple suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Collections.Generic;
  3.
  4. namespace Chap3 {
  5.     class Program {
  6.         static void Main(string[] args) {
  7.             // création d'un dictionnaire <string,int>
  8.             string[] liste = { "jean:20", "paul:18", "mélanie:10", "violette:15" };
  9.             string[] champs = null;
  10.             char[] séparateurs = new char[] { ':' };
  11.             Dictionary<string,int> dico = new Dictionary<string,int>();
  12.             for (int i = 0; i <liste.Length; i++) {
  13.                 champs = liste[i].Split(séparateurs);
  14.                 dico[champs[0]]= int.Parse(champs[1]);
  15.             }//for
  16.             // nbre d'éléments dans le dictionnaire
  17.             Console.WriteLine("Le dictionnaire a " + dico.Count + " éléments");
  18.             // liste des clés
  19.             Affiche("[Liste des clés]",dico.Keys);
  20.             // liste des valeurs
  21.             Affiche("[Liste des valeurs]", dico.Values);
  22.             // liste des clés & valeurs
  23.             Console.WriteLine("[Liste des clés & valeurs]");
  24.             foreach (string clé in dico.Keys) {
  25.                 Console.WriteLine("clé=" + clé + " valeur=" + dico[clé]);
  26.             }
  27.             // on supprime la clé "paul"
  28.             Console.WriteLine("[Suppression d'une clé]");
  29.             dico.Remove("paul");
  30.             // liste des clés & valeurs
  31.             Console.WriteLine("[Liste des clés & valeurs]");
  32.             foreach (string clé in dico.Keys) {
  33.                 Console.WriteLine("clé=" + clé + " valeur=" + dico[clé]);
  34.             }
  35.             // recherche dans le dictionnaire
  36.             String nomCherché = null;
  37.             Console.Write("Nom recherché (rien pour arrêter) : ");
  38.             nomCherché = Console.ReadLine().Trim();
  39.             int value;
  40.             while (!nomCherché.Equals("")) {
  41.                 dico.TryGetValue(nomCherché, out value);
  42.                 if (value!=0) {
  43.                     Console.WriteLine(nomCherché + "," + value);
  44.                 } else {
  45.                     Console.WriteLine("Nom " + nomCherché + " inconnu");
  46.                 }
  47.                 // recherche suivante
  48.                 Console.Out.Write("Nom recherché (rien pour arrêter) : ");
  49.                 nomCherché = Console.ReadLine().Trim();
  50.             }//while
  51.         }
  52.
  53.         // méthode générique pour afficher les éléments d'un type énumérable
  54.         static void Affiche<T>(string texte, IEnumerable<T> éléments) {
  55.             Console.WriteLine(texte.PadRight(50, '-'));
  56.             foreach (T élément in éléments) {
  57.                 Console.WriteLine(élément);
  58.             }
  59.         }
  60.
  61.     }
  62. }
  • ligne 8 : un tableau de string qui va servir à initialiser le dictionnaire <string,int>
  • ligne 11 : le dictionnaire <string,int>
  • lignes 12-15 : son initialisation à partir du tableau de string de la ligne 8
  • ligne 17 : nombre d'entrées du dictionnaire
  • ligne 19 : les clés du dictionnaire
  • ligne 21 : les valeurs du dictionnaire
  • ligne 29 : suppression d'une entrée du dictionnaire
  • ligne 41 : recherche d'une clé dans le dictionnaire. Si elle n'existe pas, la méthode TryGetValue mettra 0 dans value , car value est de type numérique. Cette technique n'est utilisable ici que parce qu'on sait que la valeur 0 n'est pas dans le dictionnaire.

Les résultats d'exécution sont les suivants :

Cacher Sélectionnez
  1. Le dictionnaire a 4 éléments
  2. [Liste des clés]----------------------------------
  3. jean
  4. paul
  5. mélanie
  6. violette
  7. [Liste des valeurs]-------------------------------
  8. 20
  9. 18
  10. 10
  11. 15
  12. [Liste des clés & valeurs]
  13. clé=jean valeur=20
  14. clé=paul valeur=18
  15. clé=mélanie valeur=10
  16. clé=violette valeur=15
  17. [Suppression d'une clé]
  18. [Liste des clés & valeurs]
  19. clé=jean valeur=20
  20. clé=mélanie valeur=10
  21. clé=violette valeur=15
  22. Nom recherché (rien pour arrêter) : violette
  23. violette,15
  24. Nom recherché (rien pour arrêter) : x
  25. Nom x inconnu

VI-E. Les fichiers texte

VI-E-1. La classe StreamReader

La classe System.IO.StreamReader permet de lire le contenu d'un fichier texte. Elle est en fait capable d'exploiter des flux qui ne sont pas des fichiers. Voici quelques-unes de ses propriétés et méthodes :

Constructeurs

public StreamReader(string path) construit un flux de lecture à partir du fichier de chemin path . Le contenu du fichier peut être encodé de diverses façons. Il existe un constructeur qui permet de préciser le codage utilisé. Par défaut, c'est le codage UTF-8 qui est utilisé.

Propriétés

public bool EndOfStream {get;} True si le flux a été lu entièrement

Méthodes

public void Close() | ferme le flux et libère les ressources allouées pour sa gestion. A faire obligatoirement après exploitation du flux.
public override int Peek() | rend le caractère suivant du flux sans le consommer. Un Peek supplémentaire rendrait donc le même caractère.
public override int Read() | rend le caractère suivant du flux et avance d'un caractère dans le flux.
public override int Read(char[] buffer, int index, int count) | lit count caractères dans le flux et les met dans buffer à partir de la position index . Rend le nombre de caractères lus - peut être 0.
public override string ReadLine() | rend la ligne suivante du flux ou null si on était à la fin du flux.
public override string ReadToEnd() | rend la fin du flux ou "" si on était à la fin du flux.

Voici un exemple :

Cacher Sélectionnez
  1. using System;
  2. using System.IO;
  3.
  4. namespace Chap3 {
  5.     class Program {
  6.         static void Main(string[] args) {
  7.             // répertoire d'exécution
  8.             Console.WriteLine("Répertoire d'exécution : "+Environment.CurrentDirectory);
  9.             string ligne = null;
  10.             StreamReader fluxInfos = null;
  11.             // lecture contenu du fichier infos.txt
  12.             try {
  13.                 // lecture 1
  14.                 Console.WriteLine("Lecture 1----------------");
  15.                 using (fluxInfos = new StreamReader("infos.txt")) {
  16.                     ligne = fluxInfos.ReadLine();
  17.                     while (ligne != null) {
  18.                         Console.WriteLine(ligne);
  19.                         ligne = fluxInfos.ReadLine();
  20.                     }
  21.                 }
  22.                 // lecture 2
  23.                 Console.WriteLine("Lecture 2----------------");
  24.                 using (fluxInfos = new StreamReader("infos.txt")) {
  25.                     Console.WriteLine(fluxInfos.ReadToEnd());
  26.                 }
  27.             } catch (Exception e) {
  28.                 Console.WriteLine("L'erreur suivante s'est produite : " + e.Message);
  29.             }
  30.         }
  31.     }
  32. }
  • ligne 8 : affiche le nom du répertoire d'exécution
  • lignes 12, 27 : un try / catch pour gérer une éventuelle exception.
  • ligne 15 : la structure using flux=new StreamReader(…) est une facilité pour ne pas avoir à fermer explicitement le flux après son exploitation. Cette fermeture est faite automatiquement dès qu'on sort de la portée du using .
  • ligne 15 : le fichier lu s'appelle infos.txt . Comme c'est un nom relatif, il sera cherché dans le répertoire d'exécution affiché par la ligne 8. S'il n'y est pas, une exception sera lancée et gérée par le try / catch.
  • lignes 16-20 : le fichier est lu par lignes successives
  • ligne 25 : le fichier est lu d'un seul coup

Le fichier infos.txt est le suivant :

Cacher Sélectionnez
  1. 12620:0:0
  2. 13190:0,05:631
  3. 15640:0,1:1290,5

et placé dans le dossier suivant du projet C# :

Image non disponible

On va découvrir que bin/Release est le dossier d'exécution lorsque le projet est excécuté par Ctrl-F5.

L'exécution donne les résultats suivants :

Cacher Sélectionnez
  1. Répertoire d'exécution : C:\data\2007-2008\c# 2008\poly\Chap3\07\bin\Release
  2. Lecture 1----------------
  3. 12620:0:0
  4. 13190:0,05:631
  5. 15640:0,1:1290,5
  6. Lecture 2----------------
  7. 12620:0:0
  8. 13190:0,05:631
  9. 15640:0,1:1290,5

Si ligne 15, on met le nom de fichier xx.txt on a les résultats suivants :

Cacher Sélectionnez
  1. Répertoire d'exécution : C:\data\2007-2008\c# 2008\poly\Chap3\07\bin\Release
  2. Lecture 1----------------
  3. L'erreur suivante s'est produite : Could not find file 'C:\...\Chap3\07\bin\Release\xx.txt'.

VI-E-2. La classe StreamWriter

La classe System.IO.StreamReader permet d'écrire dans un fichier texte. Comme la classe StreamReader , elle est en fait capable d'exploiter des flux qui ne sont pas des fichiers. Voici quelques-unes de ses propriétés et méthodes :

Constructeurs

public StreamWriter(string path) construit un flux d'écriture dans le fichier de chemin path . Le contenu du fichier peut être encodé de diverses façons. Il existe un constructeur qui permet de préciser le codage utilisé. Par défaut, c'est le codage UTF-8 qui est utilisé.

Propriétés

public virtual bool AutoFlush
{get;set;} 		fixe le mode d'écriture dans le fichier du buffer associé au flux. Si égal à False, l'écriture dans le flux n'est pas immédiate : il y a d'abord écriture dans une mémoire tampon puis dans le fichier lorsque la mémoire tampon est pleine sinon l'écriture dans le fichier est immédiate (pas de tampon intermédiaire). Par défaut c'est le mode tamponné qui est utilisé. Le tampon n'est écrit dans le fichier que lorsqu'il est plein ou bien lorsqu'on le vide explicitement par une opération Flush ou encore lorsqu'on ferme le flux StreamWriter par une opération Close. Le mode AutoFlush=False est le plus efficace lorsqu'on travaille avec des fichiers parce qu'il limite les accès disque. C'est le mode par défaut pour ce type de flux. Le mode AutoFlush=False ne convient pas à tous les flux, notamment les flux réseau. Pour ceux-ci, qui souvent prennent place dans un dialogue entre deux partenaires, ce qui est écrit par l'un des partenaires doit être immédiatement lu par l'autre. Le flux d'écriture doit alors être en mode AutoFlush=True.
public virtual string NewLine {get;set;} les caractères de fin de ligne. Par défaut "\r\n". Pour un système Unix, il faudrait utiliser "\n".

Méthodes

public void Close() | ferme le flux et libère les ressources allouées pour sa gestion. A faire obligatoirement après exploitation du flux.
public override void Flush() | écrit dans le fichier, le buffer du flux, sans attendre qu'il soit plein.
public virtual void Write(T value) | écrit value dans le fichier associé au flux. Ici T n'est pas un type générique mais symbolise le fait que la méthode Write accepte différents types de paramètres (string, int, object…). La méthode value.ToString est utilisée pour produire la chaîne écrite dans le fichier.
public virtual void WriteLine(T value) | même chose que Write mais avec la marque de fin de ligne (NewLine) en plus.

Considérons l'exemple suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.IO;
  3.
  4. namespace Chap3 {
  5.     class Program2 {
  6.         static void Main(string[] args) {
  7.             // répertoire d'exécution
  8.             Console.WriteLine("Répertoire d'exécution : " + Environment.CurrentDirectory);
  9.             string ligne = null;                        // une ligne de texte
  10.             StreamWriter fluxInfos = null;    // le fichier texte
  11.             try {
  12.                 // création du fichier texte
  13.                 using (fluxInfos = new StreamWriter("infos2.txt")) {
  14.                     Console.WriteLine("Mode AutoFlush : {0}", fluxInfos.AutoFlush);
  15.                     // lecture ligne tapée au clavier
  16.                     Console.Write("ligne (rien pour arrêter) : ");
  17.                     ligne = Console.ReadLine().Trim();
  18.                     // boucle tant que la ligne saisie est non vide
  19.                     while (ligne != "") {
  20.                         // écriture ligne dans fichier texte
  21.                         fluxInfos.WriteLine(ligne);
  22.                         // lecture nouvelle ligne au clavier
  23.                         Console.Write("ligne (rien pour arrêter) : ");
  24.                         ligne = Console.ReadLine().Trim();
  25.                     }//while
  26.                 }
  27.             } catch (Exception e) {
  28.                 Console.WriteLine("L'erreur suivante s'est produite : " + e.Message);
  29.             }
  30.         }
  31.     }
  32. }
  • ligne 13 : de nouveau, nous utilisons la syntaxe using(flux) afin de ne pas avoir à fermer explicitement le flux par une opération Close . Cette fermeture est faite automatiquement à la sortie du using .
  • pourquoi un try / catch , lignes 11 et 27 ? ligne 13, nous pourrions donner un nom de fichier sous la forme /rep1/rep2/ …/ fichier avec un chemin /rep1/rep2/… qui n'existe pas, rendant ainsi impossible la création de fichier . Une exception serait alors lancée. Il existe d'autres cas d'exception possible (disque plein, droits insuffisants…)

Les résultats d'exécution sont les suivants :

Cacher Sélectionnez
  1. Répertoire d'exécution : C:\data\2007-2008\c# 2008\poly\Chap3\07\bin\Release
  2. Mode AutoFlush : False
  3. ligne (rien pour arrêter) : 1ère ligne
  4. ligne (rien pour arrêter) : 2ième ligne
  5. ligne (rien pour arrêter) :

Le fichier infos2.txt a été créé dans le dossier bin/Release du projet :

Image non disponible

VI-F. Les fichiers binaires

Les classes System.IO.BinaryReader et System.IO.BinaryWriter servent à lire et écrire des fichiers binaires.

Considérons l'application suivante :

Cacher Sélectionnez
  1. // syntaxe pg texte bin logs
  2. // on lit un fichier texte (texte) et on range son contenu dans un fichier binaire (bin
  3. // le fichier texte a des lignes de la forme nom : age qu'on rangera dans une structure string, int
  4. // (logs) est un fichier texte de logs

Le fichier texte a le contenu suivant :

Cacher Sélectionnez
  1. paul : 10
  2. helene : 15
  3.
  4. jacques : 11
  5. sylvain : 12
  6. xx : -1
  7.
  8. xx: yy : zz
  9. xx : yy

Le programme est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.IO;
  3.
  4. // syntaxe pg texte bin logs
  5. // on lit un fichier texte (texte) et on range son contenu dans un fichier binaire (bin)
  6. // le fichier texte a des lignes de la forme nom : age qu'on rangera dans une structure string, int
  7. // (logs) est un fichier texte de logs
  8.
  9. namespace Chap3 {
  10.     class Program {
  11.         static void Main(string[] arguments) {
  12.             // il faut 3 arguments
  13.             if (arguments.Length != 3) {
  14.                 Console.WriteLine("syntaxe : pg texte binaire log");
  15.                 Environment.Exit(1);
  16.             }//if
  17.
  18.             // variables
  19.             string ligne=null;
  20.             string nom=null;
  21.             int age=0;
  22.             int numLigne = 0;
  23.             char[] séparateurs = new char[] { ':' };
  24.             string[] champs=null;
  25.             StreamReader input = null;
  26.             BinaryWriter output = null;
  27.             StreamWriter logs = null;
  28.             bool erreur = false;
  29.             // lecture fichier texte - écriture fichier binaire
  30.             try {
  31.                 // ouverture du fichier texte en lecture
  32.                 input = new StreamReader(arguments[0]);
  33.                 // ouverture du fichier binaire en écriture
  34.                 output = new BinaryWriter(new FileStream(arguments[1], FileMode.Create, FileAccess.Write));
  35.                 // ouverture du fichier des logs en écriture
  36.                 logs = new StreamWriter(arguments[2]);
  37.                 // exploitation du fichier texte
  38.                 while ((ligne = input.ReadLine()) != null) {
  39.                     // une ligne de plus
  40.                     numLigne++;
  41.                     // ligne vide ?
  42.                     if (ligne.Trim() == "") {
  43.                         // on ignore
  44.                         continue;
  45.                     }
  46.                     // une ligne nom : age
  47.                     champs = ligne.Split(séparateurs);
  48.                     // il nous faut 2 champs
  49.                     if (champs.Length != 2) {
  50.                         // on logue l'erreur
  51.                         logs.WriteLine("La ligne n° [{0}] du fichier [{1}] a un nombre de champs incorrect", numLigne, arguments[0]);
  52.                         // ligne suivante
  53.                         continue;
  54.                     }//if
  55.                     // le 1er champ doit être non vide
  56.                     erreur = false;
  57.                     nom = champs[0].Trim();
  58.                     if (nom == "") {
  59.                         // on logue l'erreur
  60.                         logs.WriteLine("La ligne n° [{0}] du fichier [{1}] a un nom vide", numLigne, arguments[0]);
  61.                         erreur = true;
  62.                     }
  63.                     // le second champ doit être un entier >=0
  64.                     if (!int.TryParse(champs[1],out age) || age<0) {
  65.                         // on logue l'erreur
  66.                         logs.WriteLine("La ligne n° [{0}] du fichier [{1}] a un âge [{2}] incorrect", numLigne, arguments[0], champs[1].Trim());
  67.                         erreur = true;
  68.                     }//if
  69.                     // si pas d'erreur, on écrit les données dans le fichier binaire
  70.                     if (!erreur) {
  71.                         output.Write(nom);
  72.                         output.Write(age);
  73.                     }
  74.                     // ligne suivante
  75.                 }//while
  76.             }catch(Exception e){
  77.                 Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
  78.             } finally {
  79.                 // fermeture des fichiers
  80.                 if(input!=null) input.Close();
  81.                 if(output!=null) output.Close();
  82.                 if(logs!=null) logs.Close();
  83.             }
  84.         }
  85.     }
  86. }

Attardons-nous sur les opérations concernant la classe BinaryWriter :

  • ligne 34 : l'objet BinaryWriter est ouvert par l'opération
Cacher Sélectionnez
  1.             output=new BinaryWriter(new FileStream(arguments[1],FileMode.Create,FileAccess.Write));

L'argument du constructeur doit être un flux (Stream). Ici c'est un flux construit à partir d'un fichier (FileStream) dont on donne :

    • le nom
    • l'opération à faire, ici FileMode.Create pour créer le fichier
    • le type d'accès, ici FileAccess.Write pour un accès en écriture au fichier
Cacher Sélectionnez
  1.             // on écrit les données dans le fichier binaire
  2.             output.Write(nom);
  3.             output.Write(age);

La classe BinaryWriter dispose de différentes méthodes Write surchargées pour écrire les différents types de données simples

  • ligne 81 : l'opération de fermeture du flux
Cacher Sélectionnez
  1.         output.Close();

Les trois arguments de la méthode Main sont donnés au projet (via ses propriétés) [1] et le fichier texte à exploiter est placé dans le dossier bin/Release [2] :

Image non disponible

Avec le fichier [personnes1.txt] suivant :

Cacher Sélectionnez
  1. paul : 10
  2. helene : 15
  3.
  4. jacques : 11
  5. sylvain : 12
  6. xx : -1
  7.
  8. xx: yy : zz
  9. xx : yy

les résultats de l'exécution sont les suivants :

Image non disponible
  • en [1], le fichier binaire [personnes1.bin] créé ainsi que le fichier de logs [logs.txt]. Celui-ci a le contenu suivant :
Cacher Sélectionnez
  1. La ligne n° [6] du fichier [personnes1.txt] a un âge [-1] incorrect
  2. La ligne n° [8] du fichier [personnes1.txt] a un nombre de champs incorrect
  3. La ligne n° [9] du fichier [personnes1.txt] a un âge [yy] incorrect

Le contenu du fichier binaire [personnes1.bin] va nous être donné par le programme qui suit. Celui-ci accepte également trois arguments :

Cacher Sélectionnez
  1. // syntaxe pg bin texte logs
  2. // on lit un fichier binaire bin et on range son contenu dans un fichier texte (texte)
  3. // le fichier binaire a une structure string, int
  4. // le fichier texte a des lignes de la forme nom : age
  5. // logs est un fichier texte de logs

On fait donc l'opération inverse. On lit un fichier binaire pour créer un fichier texte. Si le fichier texte produit est identique au fichier originel on saura que la conversion texte --> binaire --> texte s'est bien passée. Le code est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.IO;
  3.
  4. // syntaxe pg bin texte logs
  5. // on lit un fichier binaire bin et on range son contenu dans un fichier texte (texte)
  6. // le fichier binaire a une structure string, int
  7. // le fichier texte a des lignes de la forme nom : age
  8. // logs est un fichier texte de logs
  9.
  10. namespace Chap3 {
  11.     class Program2 {
  12.         static void Main(string[] arguments) {
  13.             // il faut 3 arguments
  14.             if (arguments.Length != 3) {
  15.                 Console.WriteLine("syntaxe : pg binaire texte log");
  16.                 Environment.Exit(1);
  17.             }//if
  18.
  19.             // variables
  20.             string nom = null;
  21.             int age = 0;
  22.             int numPersonne = 1;
  23.             BinaryReader input = null;
  24.             StreamWriter output = null;
  25.             StreamWriter logs = null;
  26.             bool fini;
  27.             // lecture fichier binaire - écriture fichier texte
  28.             try {
  29.                 // ouverture du fichier binaire en lecture
  30.                 input = new BinaryReader(new FileStream(arguments[0], FileMode.Open, FileAccess.Read));
  31.                 // ouverture du fichier texte en écriture
  32.                 output = new StreamWriter(arguments[1]);
  33.                 // ouverture du fichier des logs en écriture
  34.                 logs = new StreamWriter(arguments[2]);
  35.                 // exploitation du fichier binaire
  36.                 fini = false;
  37.                 while (!fini) {
  38.                     try {
  39.                         // lecture nom
  40.                         nom = input.ReadString().Trim();
  41.                         // lecture age
  42.                         age = input.ReadInt32();
  43.                         // écriture dans fichier texte
  44.                         output.WriteLine(nom + ":" + age);
  45.                         // personne suivante
  46.                         numPersonne++;
  47.                     } catch (EndOfStreamException) {
  48.                         fini = true;
  49.                     } catch (Exception e) {
  50.                         logs.WriteLine("L'erreur suivante s'est produite à la lecture de la personne n° {0} : {1}", numPersonne, e.Message);
  51.                     }
  52.                 }//while
  53.             } catch (Exception e) {
  54.                 Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
  55.             } finally {
  56.                 // fermeture des fichiers
  57.                 if (input != null)
  58.                     input.Close();
  59.                 if (output != null)
  60.                     output.Close();
  61.                 if (logs != null)
  62.                     logs.Close();
  63.             }
  64.         }
  65.     }
  66. }

Attardons-nous sur les opérations concernant la classe BinaryReader :

  • ligne 30 : l'objet BinaryReader est ouvert par l'opération
Cacher Sélectionnez
  1.             input=new BinaryReader(new FileStream(arguments[0],FileMode.Open,FileAccess.Read));

L'argument du constructeur doit être un flux (Stream). Ici c'est un flux construit à partir d'un fichier (FileStream) dont on donne :

    • le nom
    • l'opération à faire, ici FileMode.Open pour ouvrir un fichier existant
    • le type d'accès, ici FileAccess.Read pour un accès en lecture au fichier
Cacher Sélectionnez
  1. nom=input.ReadString().Trim();
  2. age=input.ReadInt32();

La classe BinaryReader dispose de différentes méthodes ReadXX pour lire les différents types de données simples

  • ligne 60 : l'opération de fermeture du flux
Cacher Sélectionnez
  1.         input.Close();

Si on exécute les deux programmes à la chaîne transformant personnes1.txt en personnes1.bin puis personnes1.bin en personnes2.txt2 on obtient les résultats suivants :

Image non disponible
  • en [1], le projet est configuré pour exécuter la 2ième application
  • en [2], les arguments passés à Main
  • en [3], les fichiers produits par l'exécution de l'application.

Le contenu de [personnes2.txt] est le suivant :

Cacher Sélectionnez
  1. paul:10
  2. helene:15
  3. jacques:11
  4. sylvain:12

VI-G. Les expressions régulières

La classe System.Text.RegularExpressions.Regex permet l'utilisation d'expression régulières. Celles-ci permettent de tester le format d'une chaîne de caractères. Ainsi on peut vérifier qu'une chaîne représentant une date est bien au format jj/mm/aa. On utilise pour cela un modèle et on compare la chaîne à ce modèle. Ainsi dans cet exemple, j m et a doivent être des chiffres. Le modèle d'un format de date valide est alors "\d\d/\d\d/\d\d" où le symbole \d désigne un chiffre. Les symboles utilisables dans un modèle sont les suivants :

Un élément dans un modèle peut être présent en 1 ou plusieurs exemplaires. Considérons quelques exemples autour du symbole \d qui représente 1 chiffre :

modèle | signification
\d | un chiffre
\d? | 0 ou 1 chiffre
\d* | 0 ou davantage de chiffres
\d+ | 1 ou davantage de chiffres
\d{2} | 2 chiffres
\d{3,} | au moins 3 chiffres
\d{5,7} | entre 5 et 7 chiffres

Imaginons maintenant le modèle capable de décrire le format attendu pour une chaîne de caractères :

chaîne recherchée | modèle
une date au format jj/mm/aa | \d{2}/\d{2}/\d{2}
une heure au format hh:mm:ss | \d{2}:\d{2}:\d{2}
un nombre entier non signé | \d+
un suite d'espaces éventuellement vide | \s*
un nombre entier non signé qui peut être précédé ou suivi d'espaces | \s*\d+\s*
un nombre entier qui peut être signé et précédé ou suivi d'espaces | \s*[+|-]?\s*\d+\s*
un nombre réel non signé qui peut être précédé ou suivi d'espaces | \s*\d+(.\d*)?\s*
un nombre réel qui peut être signé et précédé ou suivi d'espaces | \s*[+|]?\s*\d+(.\d*)?\s*
une chaîne contenant le mot juste | \b juste \b

On peut préciser où on recherche le modèle dans la chaîne :

modèle | signification
^modèle | le modèle commence la chaîne
modèle$ | le modèle finit la chaîne
^modèle$ | le modèle commence et finit la chaîne
modèle | le modèle est cherché partout dans la chaîne en commençant par le début de celle-ci.
chaîne recherchée | modèle
une chaîne se terminant par un point d'exclamation | !$
une chaîne se terminant par un point | \.$
une chaîne commençant par la séquence // | ^//
une chaîne ne comportant qu'un mot éventuellement suivi ou précédé d'espaces | ^\s*\w+\s*$
une chaîne ne comportant deux mot éventuellement suivis ou précédés d'espaces | ^\s*\w+\s*\w+\s*$
une chaîne contenant le mot secret | \b secret \b

Les sous-ensembles d'un modèle peuvent être "récupérés". Ainsi non seulement, on peut vérifier qu'une chaîne correspond à un modèle particulier mais on peut récupérer dans cette chaîne les éléments correspondant aux sous-ensembles du modèle qui ont été entourés de parenthèses. Ainsi si on analyse une chaîne contenant une date jj/mm/aa et si on veut de plus récupérer les éléments jj, mm, aa de cette date on utilisera le modèle (\d\d)/(\d\d)/(\d\d).

VI-G-1. Vérifier qu'une chaîne correspond à un modèle donné

Un objet de type Regex se construit de la façon suivante :

public Regex(string pattern) construit un objet "expression régulière" à partir d'un modèle passé en paramètre (pattern)

Une fois l'expression régulière modèle construit, on peut la comparer à des chaînes de caractères avec la méthode IsMatch :

public bool IsMatch(string input) vrai si la chaîne input correspond au modèle de l'expression régulière

Voici un exemple :

Cacher Sélectionnez
  1. using System;
  2. using System.Text.RegularExpressions;
  3.
  4. namespace Chap3 {
  5.     class Program {
  6.         static void Main(string[] args) {
  7.             // une expression régulière modèle
  8.             string modèle1 = @"^\s*\d+\s*$";
  9.             Regex regex1 = new Regex(modèle1);
  10.             // comparer un exemplaire au modèle
  11.             string exemplaire1 = "  123  ";
  12.             if (regex1.IsMatch(exemplaire1)) {
  13.                 Console.WriteLine("[{0}] correspond au modèle [{1}]", exemplaire1, modèle1);
  14.             } else {
  15.                 Console.WriteLine("[{0}] ne correspond pas au modèle [{1}]", exemplaire1, modèle1);
  16.             }//if
  17.             string exemplaire2 = "  123a  ";
  18.             if (regex1.IsMatch(exemplaire2)) {
  19.                 Console.WriteLine("[{0}] correspond au modèle [{1}]", exemplaire2, modèle1);
  20.             } else {
  21.                 Console.WriteLine("[{0}] ne correspond pas au modèle [{1}]", exemplaire2, modèle1);
  22.             }//if
  23.         }
  24.
  25.     }
  26. }

et les résultats d'exécution :

Cacher Sélectionnez
  1. [  123  ] correspond au modèle [^\s*\d+\s*$]
  2. [  123a  ] ne correspond pas au modèle [^\s*\d+\s*$]

VI-G-2. Trouver toutes les occurrences d'un modèle dans une chaîne

La méthode Matches permet de récupérer les éléments d'une chaîne correspondant à un modèle :

public MatchCollection Matches(string input) rend la collection des éléments de la chaîne input correspondant au modèle

La classe MatchCollection a une propriété Count qui est le nombre d'éléments de la collection. Si résultats est un objet MatchCollection , résultats[i] est l'élément i de cette collection et est de type Match. La classe Match a diverses propriétés dont les suivantes :

  • Value : la valeur de l'objet Match, donc un élément correspondant au modèle
  • Index : la position où l'élément a été trouvé dans la chaîne explorée

Examinons l'exemple suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Text.RegularExpressions;
  3.
  4. namespace Chap3 {
  5.     class Program2 {
  6.         static void Main(string[] args) {
  7.             // plusieurs occurrences du modèle dans l'exemplaire
  8.             string modèle2 = @"\d+";
  9.             Regex regex2 = new Regex(modèle2);
  10.             string exemplaire3 = "  123  456  789 ";
  11.             MatchCollection résultats = regex2.Matches(exemplaire3);
  12.             Console.WriteLine("Modèle=[{0}],exemplaire=[{1}]", modèle2, exemplaire3);
  13.             Console.WriteLine("Il y a {0} occurrences du modèle dans l'exemplaire ", résultats.Count);
  14.             for (int i = 0; i < résultats.Count; i++) {
  15.                 Console.WriteLine("[{0}] trouvé en position {1}", résultats[i].Value, résultats[i].Index);
  16.             }//for
  17.         }
  18.     }
  19. }
  • ligne 8 : le modèle recherché est une suite de chiffres
  • ligne 10 : la chaîne dans laquelle on recherche ce modèle
  • ligne 11 : on récupère tous les éléments de exemplaire3 vérifiant le modèle modèle2
  • lignes 14-16 : on les affiche

Les résultats de l'exécution du programme sont les suivants :

Cacher Sélectionnez
  1. Modèle=[\d+],exemplaire=[  123  456  789 ]
  2. Il y a 3 occurrences du modèle dans l'exemplaire
  3. [123] trouvé en position 2
  4. [456] trouvé en position 7
  5. [789] trouvé en position 12

VI-G-3. Récupérer des parties d'un modèle

Des sous-ensembles d'un modèle peuvent être "récupérés". Ainsi non seulement, on peut vérifier qu'une chaîne correspond à un modèle particulier mais on peut récupérer dans cette chaîne les éléments correspondant aux sous-ensembles du modèle qui ont été entourés de parenthèses. Ainsi si on analyse une chaîne contenant une date jj/mm/aa et si on veut de plus récupérer les éléments jj, mm, aa de cette date on utilisera le modèle (\d\d)/(\d\d)/(\d\d).

Examinons l'exemple suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Text.RegularExpressions;
  3.
  4. namespace Chap3 {
  5.     class Program3 {
  6.         static void Main(string[] args) {
  7.             // capture d'éléments dans le modèle
  8.             string modèle3 = @"(\d\d):(\d\d):(\d\d)";
  9.             Regex regex3 = new Regex(modèle3);
  10.             string exemplaire4 = "Il est 18:05:49";
  11.             // vérification modèle
  12.             Match résultat = regex3.Match(exemplaire4);
  13.             if (résultat.Success) {
  14.                 // l'exemplaire correspond au modèle
  15.                 Console.WriteLine("L'exemplaire [{0}] correspond au modèle [{1}]",exemplaire4,modèle3);
  16.                 // on affiche les groupes de parenthèses
  17.                 for (int i = 0; i < résultat.Groups.Count; i++) {
  18.                     Console.WriteLine("groupes[{0}]=[{1}] trouvé en position {2}",i, résultat.Groups[i].Value,résultat.Groups[i].Index);
  19.                 }//for
  20.             } else {
  21.                 // l'exemplaire ne correspond pas au modèle
  22.                 Console.WriteLine("L'exemplaire[{0}] ne correspond pas au modèle [{1}]", exemplaire4, modèle3);
  23.             }
  24.         }
  25.     }
  26. }

L'exécution de ce programme produit les résultats suivants :

Cacher Sélectionnez
  1. L'exemplaire [Il est 18:05:49] correspond au modèle [(\d\d):(\d\d):(\d\d)]
  2. groupes[0]=[18:05:49] trouvé en position 7
  3. groupes[1]=[18] trouvé en position 7
  4. groupes[2]=[05] trouvé en position 10
  5. groupes[3]=[49] trouvé en position 13

La nouveauté se trouve dans les lignes 12-19 :

  • ligne 12 : la chaîne exemplaire4 est comparée au modèle regex3 au travers de la méthode Match . Celle-ci rend un objet Match déjà présenté. Nous utilisons ici deux nouvelles propriétés de cette classe :
  • Success (ligne 13) : indique s'il y a eu correspondance
  • Groups (lignes 17, 18) : collection où
    • Groups[0] correspond à la partie de la chaîne correspondant au modèle
    • Groups[i] (i>=1) correspond au groupe de parenthèses n° i

Si résultat est de type Match , résultats.Groups est de type GroupCollection et résultats.Groups[i] de type Group . La classe Group a deux propriétés que nous utilisons ici :

  • Value (ligne 18) : la valeur de l'objet Group qui est l'élément correspondant au contenu d'une parenthèse
  • Index (ligne 18) : la position où l'élément a été trouvé dans la chaîne explorée

VI-G-4. Un programme d'apprentissage

Trouver l'expression régulière qui permet de vérifier qu'une chaîne correspond bien à un certain modèle est parfois un véritable défi. Le programme suivant permet de s'entraîner. Il demande un modèle et une chaîne et indique si la chaîne correspond ou non au modèle.

Cacher Sélectionnez
  1. using System;
  2. using System.Text.RegularExpressions;
  3.
  4. namespace Chap3 {
  5.     class Program4 {
  6.         static void Main(string[] args) {
  7.             // données
  8.             string modèle, chaine;
  9.             Regex regex = null;
  10.             MatchCollection résultats;
  11.             // on demande à l'utilisateur les modèles et les exemplaires à comparer à celui-ci
  12.             while (true) {
  13.                 // on demande le modèle
  14.                 Console.Write("Tapez le modèle à tester ou rien pour arrêter :");
  15.                 modèle = Console.In.ReadLine();
  16.                 // fini ?
  17.                 if (modèle.Trim() == "")
  18.                     break;
  19.                 // on crée l'expression régulière
  20.                 try {
  21.                     regex = new Regex(modèle);
  22.                 } catch (Exception ex) {
  23.                     Console.WriteLine("Erreur : " + ex.Message);
  24.                     continue;
  25.                 }
  26.                 // on demande à l'utilisateur les exemplaires à comparer au modèle
  27.                 while (true) {
  28.                     Console.Write("Tapez la chaîne à comparer au modèle [{0}] ou rien pour arrêter :", modèle);
  29.                     chaine = Console.ReadLine();
  30.                     // fini ?
  31.                     if (chaine.Trim() == "")
  32.                         break;
  33.                     // on fait la comparaison
  34.                     résultats = regex.Matches(chaine);
  35.                     // succès ?
  36.                     if (résultats.Count == 0) {
  37.                         Console.WriteLine("Je n'ai pas trouvé de correspondances");
  38.                         continue;
  39.                     }//if
  40.                     // on affiche les éléments correspondant au modèle
  41.                     for (int i = 0; i < résultats.Count; i++) {
  42.                         Console.WriteLine("J'ai trouvé la correspondance [{0}] en position [{1}]", résultats[i].Value, résultats[i].Index);
  43.                         // des sous-éléments
  44.                         if (résultats[i].Groups.Count != 1) {
  45.                             for (int j = 1; j < résultats[i].Groups.Count; j++) {
  46.                                 Console.WriteLine("\tsous-élément [{0}] en position [{1}]", résultats[i].Groups[j].Value, résultats[i].Groups[j].Index);
  47.                             }
  48.                         }
  49.                     }
  50.                 }
  51.             }
  52.         }
  53.     }
  54. }

Voici un exemple d'exécution :

Cacher Sélectionnez
  1. Tapez le modèle à tester ou rien pour arrêter :\d+
  2. Tapez la chaîne à comparer au modèle [\d+] ou rien pour arrêter :123 456 789
  3. J'ai trouvé la correspondance [123] en position [0]
  4. J'ai trouvé la correspondance [456] en position [4]
  5. J'ai trouvé la correspondance [789] en position [8]
  6. Tapez la chaîne à comparer au modèle [\d+] ou rien pour arrêter :
  7. Tapez le modèle à tester ou rien pour arrêter :(\d{2}):(\d\d)
  8. Tapez la chaîne à comparer au modèle [(\d{2}):(\d\d)] ou rien pour arrêter :14:15 abcd 17:18 xyzt
  9. J'ai trouvé la correspondance [14:15] en position [0]
  10.         sous-élément [14] en position [0]
  11.         sous-élément [15] en position [3]
  12. J'ai trouvé la correspondance [17:18] en position [11]
  13.         sous-élément [17] en position [11]
  14.         sous-élément [18] en position [14]
  15. Tapez la chaîne à comparer au modèle [(\d{2}):(\d\d)] ou rien pour arrêter :
  16. Tapez le modèle à tester ou rien pour arrêter :^\s*\d+\s*$
  17. Tapez la chaîne à comparer au modèle [^\s*\d+\s*$] ou rien pour arrêter :   1456
  18. J'ai trouvé la correspondance [   1456] en position [0]
  19. Tapez la chaîne à comparer au modèle [^\s*\d+\s*$] ou rien pour arrêter :
  20. Tapez le modèle à tester ou rien pour arrêter :^\s*(\d+)\s*$
  21. Tapez la chaîne à comparer au modèle [^\s*(\d+)\s*$] ou rien pour arrêter :1456
  22. J'ai trouvé la correspondance [1456] en position [0]
  23.         sous-élément [1456] en position [0]
  24. Tapez la chaîne à comparer au modèle [^\s*(\d+)\s*$] ou rien pour arrêter :abcd 1456
  25. Je n'ai pas trouvé de correspondances
  26. Tapez la chaîne à comparer au modèle [^\s*(\d+)\s*$] ou rien pour arrêter :
  27. Tapez le modèle à tester ou rien pour arrêter :

VI-G-5. La méthode Split

Nous avons déjà rencontré cette méthode dans la classe String :

public string [] Split( char [] separator) la chaîne est vue comme une suite de champs séparés par les caractères présents dans le tableau separator . Le résultat est le tableau de ces champs

La méthode Split de la classe Regex nous permet d'exprimer le séparateur en fonction d'un modèle :

public string [] Split(string input) La chaîne input est décomposée en champs, ceux-ci étant séparés par un séparateur correspondant au modèle de l'objet Regex courant.

Supposons par exemple qu'on ait dans un fichier texte des lignes de la forme champ1, champ2, .., champn . Les champs sont séparés par une virgule mais celle-ci peut être précédée ou suivie d'espaces. La méthode Split de la classe string ne convient alors pas. Celle de la méthode RegEx apporte la solution. Si ligne est la ligne lue, les champs pourront être obtenus par

Cacher Sélectionnez
  1. string[] champs=new Regex(@"s*,\s*").Split(ligne);

comme le montre l'exemple suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Text.RegularExpressions;
  3.
  4. namespace Chap3 {
  5.     class Program5 {
  6.         static void Main(string[] args) {
  7.             // une ligne
  8.             string ligne = "abc  , def  , ghi";
  9.             // un modèle
  10.             Regex modèle = new Regex(@"\s*,\s*");
  11.             // décomposition de ligne en champs
  12.             string[] champs = modèle.Split(ligne);
  13.             // affichage
  14.             for (int i = 0; i < champs.Length; i++) {
  15.                 Console.WriteLine("champs[{0}]=[{1}]", i, champs[i]);
  16.             }
  17.         }
  18.     }
  19. }

Les résultats d'exécution :

Cacher Sélectionnez
  1. champs[0]=[abc]
  2. champs[1]=[def]
  3. champs[2]=[ghi]

VI-H. Application exemple - version 3

Nous reprenons l'application étudiée aux paragraphes page (version 1) et page (version 2).

Dans la dernière version étudiée, le calcul de l'impôt se faisait dans la classe abstraite AbstractImpot :

Cacher Sélectionnez
  1. namespace Chap2 {
  2.     abstract class AbstractImpot : IImpot {
  3.
  4.         // les tranches d'impôt nécessaires au calcul de l'impôt
  5.         // proviennent d'une source extérieure
  6.
  7.         protected TrancheImpot[] tranchesImpot;
  8.
  9.         // calcul de l'impôt
  10.         public int calculer(bool marié, int nbEnfants, int salaire) {
  11.             // calcul du nombre de parts
  12.             decimal nbParts;
  13.             if (marié) nbParts = (decimal)nbEnfants / 2 + 2;
  14.             else nbParts = (decimal)nbEnfants / 2 + 1;
  15.             if (nbEnfants >= 3) nbParts += 0.5M;
  16.             // calcul revenu imposable & Quotient familial
  17.             decimal revenu = 0.72M * salaire;
  18.             decimal QF = revenu / nbParts;
  19.             // calcul de l'impôt
  20.             tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
  21.             int i = 0;
  22.             while (QF > tranchesImpot[i].Limite) i++;
  23.             // retour résultat
  24.             return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
  25.         }//calculer
  26.     }//classe
  27.
  28. }

La méthode calculer de la ligne 38 utilise le tableau tranchesImpot de la ligne 35, tableau non initialisé par la classe AbstractImpot . C'est pourquoi elle est abstraite et doit être dérivée pour être utile. Cette initialisation était faite par la classe dérivée HardwiredImpot :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     class HardwiredImpot : AbstractImpot {
  5.
  6.         // tableaux de données nécessaires au calcul de l'impôt
  7.         decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
  8.         decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
  9.         decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };
  10.
  11.         public HardwiredImpot() {
  12.                 // création du tableau des tranches d'impôt
  13.             tranchesImpot = new TrancheImpot[limites.Length];
  14.                 // remplissage
  15.             for (int i = 0; i < tranchesImpot.Length; i++) {
  16.                 tranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
  17.                 }
  18.         }
  19.     }// classe
  20. }// namespace

Ci-dessus, les données nécessaires au calcul de l'impôt étaient placées en "dur" dans le code de la classe. La nouvelle version de l'exemple les place dans un fichier texte :

Cacher Sélectionnez
  1. 4962:0:0
  2. 8382:0,068:291,09
  3. 14753:0,191:1322,92
  4. 23888:0,283:2668,39
  5. 38868:0,374:4846,98
  6. 47932:0,426:6883,66
  7. 0:0,481:9505,54

L'exploitation de ce fichier pouvant produire des exceptions, nous créons une classe spéciale pour gérer ces dernières :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap3 {
  4.     class FileImpotException : Exception {
  5.         // codes d'erreur
  6.         [Flags]
  7.         public enum CodeErreurs { Acces = 1, Ligne = 2, Champ1 = 4, Champ2 = 8, Champ3 = 16 };
  8.
  9.         // code d'erreur
  10.         public CodeErreurs Code { get; set; }
  11.
  12.         // constructeurs
  13.         public FileImpotException() {
  14.         }
  15.         public FileImpotException(string message)
  16.             : base(message) {
  17.         }
  18.         public FileImpotException(string message, Exception e)
  19.             : base(message,e) {
  20.         }
  21.     }
  22. }
  • ligne 4 : la classe FileImpotException dérive de la classe Exception . Elle servira à mémoriser toute erreur survenant lors de l'exploitation du fichier texte des données.
  • ligne 7 : une énumération représentant des codes d'erreur :
  • Acces : erreur d'accès au fichier texte des données
  • Ligne : ligne n'ayant pas les trois champs attendus
  • Champ1 : le champ n° 1 est erroné
  • Champ2 : le champ n° 2 est erroné
  • Champ3 : le champ n° 3 est erroné

Certaines de ces erreurs peuvent se combiner ( Champ1 , Champ2 , Champ3 ). Aussi l'énumération CodeErreurs a-t-elle été annotée avec l'attribut [Flags] qui implique que les différentes valeurs de l'énumération doivent être des puissances de 2. Une erreur sur les champs 1 et 2 se traduira alors par le code d'erreur Champ1 | Champ2 .

  • ligne 10 : la propriété automatique Code mémorisera le code de l'erreur.
  • lignes 15 : un constructeur permettant de construire un objet FileImpotException en lui passant comme paramètre un message d'erreur.
  • lignes 18 : un constructeur permettant de construire un objet FileImpotException en lui passant comme paramètres un message d'erreur et l'exception à l'origine de l'erreur.

La classe qui initialise le tableau tranchesImpot de la classe AbstractImpot est désormais la suivante :

Cacher Sélectionnez
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Text.RegularExpressions;
  5.
  6. namespace Chap3 {
  7.     class FileImpot : AbstractImpot {
  8.
  9.         public FileImpot(string fileName) {
  10.             // données
  11.             List<TrancheImpot> listTranchesImpot = new List<TrancheImpot>();
  12.             int numLigne = 1;
  13.             // exception
  14.             FileImpotException fe = null;
  15.             // lecture contenu du fichier fileName, ligne par ligne
  16.             Regex pattern = new Regex(@"s*:\s*");
  17.             // au départ pas d'erreur
  18.             FileImpotException.CodeErreurs code = 0;
  19.             try {
  20.                 using (StreamReader input = new StreamReader(fileName)) {
  21.                     while (!input.EndOfStream && code == 0) {
  22.                         // ligne courante
  23.                         string ligne = input.ReadLine().Trim();
  24.                         // on ignore les lignes vides
  25.                         if (ligne == "") continue;
  26.                         // ligne décomposée en trois champs séparés par :
  27.                         string[] champsLigne = pattern.Split(ligne);
  28.                         // a-t-on 3 champs ?
  29.                         if (champsLigne.Length != 3) {
  30.                             code = FileImpotException.CodeErreurs.Ligne;
  31.                         }
  32.                         // conversions des 3 champs
  33.                         decimal limite = 0, coeffR = 0, coeffN = 0;
  34.                         if (code == 0) {
  35.                             if (!Decimal.TryParse(champsLigne[0], out limite)) code = FileImpotException.CodeErreurs.Champ1;
  36.                             if (!Decimal.TryParse(champsLigne[1], out coeffR)) code |= FileImpotException.CodeErreurs.Champ2;
  37.                             if (!Decimal.TryParse(champsLigne[2], out coeffN)) code |= FileImpotException.CodeErreurs.Champ3; ;
  38.                         }
  39.                         // erreur ?
  40.                         if (code != 0) {
  41.                             // on note l'erreur
  42.                             fe = new FileImpotException(String.Format("Ligne n° {0} incorrecte", numLigne)) { Code = code };
  43.                         } else {
  44.                             // on mémorise la nouvelle tranche d'impôt
  45.                             listTranchesImpot.Add(new TrancheImpot() { Limite = limite, CoeffR = coeffR, CoeffN = coeffN });
  46.                             // ligne suivante
  47.                             numLigne++;
  48.                         }
  49.                     }
  50.                 }
  51.                 // on transfère la liste listImpot dans le tableau tranchesImpot
  52.                 if (code == 0) {
  53.                     // on transfère la liste listImpot dans le tableau tranchesImpot
  54.                     tranchesImpot = listTranchesImpot.ToArray();
  55.                 }
  56.             } catch (Exception e) {
  57.                 // on note l'erreur
  58.                 fe= new FileImpotException(String.Format("Erreur lors de la lecture du fichier {0}", fileName), e) { Code = FileImpotException.CodeErreurs.Acces };
  59.             }
  60.             // erreur à signaler ?
  61.             if (fe != null) throw fe;
  62.         }
  63.     }
  64. }
  • ligne 7 : la classe FileImpot dérive de la classe AbstractImpot comme le faisait dans la version 2 la classe HardwiredImpot .
  • ligne 9 : le constructeur de la classe FileImpot a pour rôle d'initialiser le champ trancheImpot de sa classe de base AbstractImpot . Il admet pour paramètre, le nom du fichier texte contenant les données.
  • ligne 11 : le champ tranchesImpot de la classe de base AbstractImpot est un tableau qui a être rempli avec les données du fichier filename passé en paramètre. La lecture d'un fichier texte est séquentielle. On ne connaît le nombre de lignes qu'après avoir lu la totalité du fichier. Aussi ne peut-on dimensionner le tableau tranchesImpot. O n mémorisera momentanément les données dans la liste générique listTranchesImpot.

On rappelle que le type TrancheImpot est une structure :

Cacher Sélectionnez
  1. namespace Chap3 {
  2.     // une tranche d'impôt
  3.     struct TrancheImpot {
  4.         public decimal Limite { get; set; }
  5.         public decimal CoeffR { get; set; }
  6.         public decimal CoeffN { get; set; }
  7.     }
  8. }
  • ligne 14 : fe de type FileImpotException sert à encapsuler une éventuelle erreur d'exploitation du fichier texte.
  • ligne 16 : l'expression régulière du séparateur de champs dans une ligne champ1:champ2:champ3 du fichier texte. Les champs sont séparés par le caractère : précédé et suivi d'un nombre quelconque d'espaces.
  • ligne 18 : le code de l'erreur en cas d'erreur
  • ligne 20 : exploitation du fichier texte avec un StreamReader
  • ligne 21 : on boucle tant qu'il reste une ligne à lire et qu'il n'y a pas eu d'erreur
  • ligne 27 : la ligne lue est divisée en champs grâce à l'expression régulière de la ligne 16
  • lignes 29-31 : on vérifie que la ligne a bien trois champs - on note une éventuelle erreur
  • lignes 33-38 : conversion des trois chaînes en trois nombres décimaux - on note les éventuelles erreurs
  • lignes 40-43 : s'il y a eu erreur, une exception de type FileImpotException est créée.
  • lignes 44-47 : s'il n'y a pas eu d'erreur, on passe à la lecture de la ligne suivante du fichier texte après avoir mémorisé les données issues de la ligne courante.
  • lignes 52-55 : à la sortie de la bouche while , les données de la liste générique listTranchesImpot sont recopiées dans le tableau tranchesImpot de la classe de base AbstractImpot . On rappelle que tel était le but du constructeur.
  • lignes 56-59 : gestion d'une éventuelle exception. Celle-ci est encapsulée dans un objet de type FileImpotException .
  • ligne 61 : si l'exception fe de la ligne 18 a été initialisée, alors elle est lancée.

L'ensemble du projet C# est le suivant :

Image non disponible
  • en [1] : l'ensemble du projet
  • en [2,3] : les propriétés du fichier [DataImpot.txt] [2]. La propriété [Copy to Output Directory] [3] est mise à always. Ceci fait que le fichier [DataImpot.txt] sera copié dans le dossier bin/Release (mode Release) ou bin/Debug (mode Debug) à chaque exécution. C'est là qu'il est cherché par l'exécutable.
  • en [4] : on fait de même avec le fichier [DataImpotInvalide.txt].

Le contenu de [DataImpot.txt] est le suivant :

Cacher Sélectionnez
  1. 4962:0:0
  2. 8382:0,068:291,09
  3. 14753:0,191:1322,92
  4. 23888:0,283:2668,39
  5. 38868:0,374:4846,98
  6. 47932:0,426:6883,66
  7. 0:0,481:9505,54

Le contenu de [DataImpotInvalide.txt] est le suivant :

Cacher Sélectionnez
  1. a:b:c

Le programme de test [Program.cs] n'a pas changé : c'est celui de la version 2 page , à la différence près suivante :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap3 {
  4.     class Program {
  5.         static void Main() {
  6. ...
  7.             // création d'un objet IImpot
  8.             IImpot impot = null;
  9.             try {
  10.                 // création d'un objet IImpot
  11.                 impot = new FileImpot("DataImpot.txt");
  12.             } catch (FileImpotException e) {
  13.                 // affichage erreur
  14.                 string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
  15.                 Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
  16.                 // arrêt programme
  17.                 Environment.Exit(1);
  18.             }
  19.
  20.             // boucle infinie
  21.             while (true) {
  22. ...
  23.             }//while
  24.         }
  25.     }
  26. }
  • ligne 8 : objet impot du type de l'interface IImpot
  • ligne 11 : instanciation de l'objet impot avec un objet de type FileImpot . Celle-ci peut générer une exception qui est gérée par le try / catch des lignes 9 / 12 / 18.

Voici des exemples d'exécution :

Avec le fichier [ DataImpot.txt]

Cacher Sélectionnez
  1. Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 60000
  2. Impot=4282 euros
  3. Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :

Avec un fichier [ xx] inexistant

Cacher Sélectionnez
  1. L'erreur suivante s'est produite : [Code=Acces,Message=Erreur lors de la lecture du fichier xx, Exception d'origine : Could not find file 'C:\data\2007-2008\c#2008\poly\Chap3\10\bin\Release\xx'.]

Avec le fichier [ DataImpotInvalide.txt]

Cacher Sélectionnez
  1. L'erreur suivante s'est produite : [Code=Champ1, Champ2, Champ3,Message=Ligne n° 1 incorrecte]


CSharp tutoriel 2008

Apprentissage du langage C#

  • VII. Architectures 3 couches
    • VII-A. Introduction
    • VII-B. Les interfaces de l'application [Impots]
    • VII-C. Application exemple - version 4
      • VII-C-1. Le projet Visual Studio
      • VII-C-2. Les entités de l'application
      • VII-C-3. La couche [dao]
      • VII-C-4. La couche [metier]
      • VII-C-5. La couche [ui]
      • VII-C-6. Conclusion
    • VII-D. Application exemple - version 5
      • VII-D-1. NUnit
      • VII-D-2. La solution Visual Studio
      • VII-D-3. La couche [dao]
      • VII-D-4. La couche [metier]
      • VII-D-5. La couche [ui]
      • VII-D-6. La couche [Spring]

VII. Architectures 3 couches

VII-A. Introduction

Revenons sur la dernière version de l'application de calcul d'impôt :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap3 {
  4.     class Program {
  5.         static void Main() {
  6.             // programme interactif de calcul d'Impot
  7.             // l'utilisateur tape trois données au clavier : marié nbEnfants salaire
  8.             // le programme affiche alors l'Impot à payer
  9. ...
  10.
  11.             // création d'un objet IImpot
  12.             IImpot impot = null;
  13.             try {
  14.                 // création d'un objet IImpot
  15.                 impot = new FileImpot("DataImpotInvalide.txt");
  16.             } catch (FileImpotException e) {
  17.                 // affichage erreur
  18. ...
  19.                 // arrêt programme
  20.                 Environment.Exit(1);
  21.             }
  22.             // boucle infinie
  23.             while (true) {
  24.                 // on demande les paramètres du calcul de l'impôt
  25.                 Console.Write("Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :");
  26.                 string paramètres = Console.ReadLine().Trim();
  27. ...
  28.                 // les paramètres sont corrects - on calcule l'Impot
  29.                 Console.WriteLine("Impot=" + impot.calculer(marié == "o", nbEnfants, salaire) + " euros");
  30.                 // contribuable suivant
  31.             }//while
  32.         }
  33.     }
  34. }

La solution précédente inclut des traitements classiques en programmation :

  • la récupération de données mémorisées dans des fichiers, bases de données… lignes 12-21
  • le dialogue avec l'utilisateur, lignes 26 (saisies) et 29 (affichages)
  • l'utilisation d'un algorithme métier, ligne 29

La pratique a montré qu'isoler ces différents traitements dans des classes séparées améliorait la maintenabilité des applications. L'architecture d'une application ainsi structurée est la suivante :

Image non disponible

On appelle cette architecture, "architecture trois tiers", traduction de l'anglais "three tier architecture". Le terme "trois tiers" désigne normalement une architecture où chaque tier est sur une machine différente. Lorsque les tiers sont sur une même machine, l'architecture devient une architecture "trois couches".

  • la couche [metier] est celle qui contient les règles métier de l'application. Pour notre application de calcul d'impôt, ce sont les règles qui permettent de calculer l'impôt d'un contribuable. Cette couche a besoin de données pour travailler :
  • les tranches d'impôt, données qui changent chaque année
  • le nombre d'enfants, le statut marital et le salaire annuel du contribuable

Dans le schéma ci-dessus, les données peuvent provenir de deux endroits :

  • la couche d'accès aux données ou [dao] (DAO = Data Access Object) pour les données déjà enregistrées dans des fichiers ou bases de données. Ce pourrait être le cas ici des tranches d'impôt comme il a été fait dans la version précédente de l'application.
  • la couche d'interface avec l'utilisateur ou [ui] (UI = User Interface) pour les données saisies par l'utilisateur ou affichées à l'utilisateur. Ce pourrait être le cas ici du nombre d'enfants, du statut marital et du salaire annuel du contribuable
  • de façon générale, la couche [dao] s'occupe de l'accès aux données persistantes (fichiers, bases de données) ou non persistantes (réseau, capteurs…).
  • la couche [ui] elle, s'occupe des interactions avec l'utilisateur s'il y en a un.
  • les trois couches sont rendues indépendantes grâce à l'utilisation d'interfaces.

Nous allons reprendre l'application [Impots] déjà étudiée à plusieurs reprises pour lui donner une architecture 3 couches. Pour cela, nous allons étudier les couches [ui, metier, dao] les unes après les autres, en commençant par la couche [dao], couche qui s'occupe des données persistantes.

Auparavant, il nous faut définir les interfaces des différentes couches de l'application [Impots].

VII-B. Les interfaces de l'application [Impots]

Rappelons qu'une interface définit un ensemble de signatures de méthodes. Les classes implémentant l'interface donnent un contenu à ces méthodes.

Revenons à l'architecture 3 couches de notre application :

Image non disponible

Dans ce type d'architecture, c'est souvent l'utilisateur qui prend les initiatives. Il fait une demande en [1] et reçoit une réponse en [8]. On appelle cela le cycle demande - réponse. Prenons l'exemple du calcul de l'impôt d'un contribuable. Celui-ci va nécessiter plusieurs étapes :

  • la couche [ui] va devoir demander à l'utilisateur son nombre d'enfants, son statut marital et son salaire annuel. C'est l'opération [1] ci-dessus.
  • ceci fait, la couche [ui] va demander à la couche métier de faire le calcul de l'impôt. Pour cela elle va lui transmettre les données qu'elle a reçues de l'utilisateur. C'est l'opération [2].
  • la couche [metier] a besoin de certaines informations pour mener à bien son travail : les tranches d'impôt. Elle va demander ces informations à la couche [dao] avec le chemin [3, 4, 5, 6]. [3] est la demande initiale et [6] la réponse à cette demande.
  • ayant toutes les données dont elle avait besoin, la couche [metier] calcule l'impôt.
  • la couche [metier] peut maintenant répondre à la demande de la couche [ui] faite en (b). C'est le chemin [7].
  • la couche [ui] va mettre en forme ces résultats puis les présenter à l'utilisateur. C'est le chemin [8].
  • on pourrait imaginer que l'utilisateur fait des simulations d'impôt et qu'il veuille mémoriser celles-ci. Il utilisera le chemin [1-8] pour le faire.

On voit dans cette description qu'une couche est amenée à utiliser les ressources de la couche qui est à sa droite, jamais de celle qui est à sa gauche. Considérons deux couches contigües :

Image non disponible

La couche [A] fait des demandes à la couche [B]. Dans les cas les plus simples, une couche est implémentée par une unique classe. Une application évolue au cours du temps. Ainsi la couche [B] peut avoir des classes d'implémentation différentes [B1, B2…]. Si la couche [B] est la couche [dao], celle-ci peut avoir une première implémentation [B1] qui va chercher des données dans un fichier. Quelques années plus tard, on peut vouloir mettre les données dans une base de données. On va alors construire une seconde classe d'implémentation [B2]. Si dans l'application initiale, la couche [A] travaillait directement avec la classe [B1] on est obligés de réécrire partiellement le code de la couche [A]. Supposons par exemple qu'on ait écrit dans la couche [A] quelque chose comme suit :

Cacher Sélectionnez
  1. B1 b1=new B1(...);
  2. ..
  3. b1.getData(...);
  • ligne 1 : une instance de la classe [B1] est créée
  • ligne 3 : des données sont demandées à cette instance

Si on suppose, que la nouvelle classe d'implémentation [B2] utilise des méthodes de même signature que celle de la classe [B1], il faudra changer tous les [B1] en [B2]. Ca, c'est le cas très favorable et assez improbable si on n'a pas prêté attention à ces signatures de méthodes. Dans la pratique, il est fréquent que les classes [B1] et [B2] n'aient pas les mêmes signatures de méthodes et que donc une bonne partie de la couche [A] doive être totalement réécrite.

On peut améliorer les choses si on met une interface entre les couches [A] et [B]. Cela signifie qu'on fige dans une interface les signatures des méthodes présentées par la couche [B] à la couche [A]. Le schéma précédent devient alors le suivant :

Image non disponible

La couche [A] ne s'adresse désormais plus directement à la couche [B] mais à son interface [IB]. Ainsi dans le code de la couche [A], la classe d'implémentation [Bi] de la couche [B] n'apparaît qu'une fois, au moment de l'implémentation de l'interface [IB]. Ceci fait, c'est l'interface [IB] et non sa classe d'implémentation qui est utilisée dans le code. Le code précédent devient celui-ci :

Cacher Sélectionnez
  1. IB ib=new B1(...);
  2. ..
  3. ib.getData(...);
  • ligne 1 : une instance [ib] implémentant l'interface [IB] est créée par instanciation de la classe [B1]
  • ligne 3 : des données sont demandées à l'instance [ib]

Désormais si on remplace l'implémentation [B1] de la couche [B] par une implémentation [B2], et que ces deux implémentations respectent la même interface [IB], alors seule la ligne 1 de la couche [A] doit être modifiée et aucune autre. C'est un grand avantage qui à lui seul justifie l'usage systématique des interfaces entre deux couches.

On peut aller encore plus loin et rendre la couche [A] totalement indépendante de la couche [B]. Dans le code ci-dessus, la ligne 1 pose problème parce qu'elle référence en dur la classe [B1]. L'idéal serait que la couche [A] puisse disposer d'une implémentation de l'interface [IB] sans avoir à nommer de classe. Ce serait cohérent avec notre schéma ci-dessus. On y voit que la couche [A] s'adresse à l'interface [IB] et on ne voit pas pourquoi elle aurait besoin de connaître le nom de la classe qui implémente cette interface. Ce détail n'est pas utile à la couche [A].

Le framework Spring (http://www.springframework.org) permet d'obtenir ce résultat. L'architecture précédente évolue de la façon suivante :

Image non disponible

La couche transversale [Spring] va permettre à une couche d'obtenir par configuration une référence sur la couche située à sa droite sans avoir à connaître le nom de la classe d'implémentation de la couche. Ce nom sera dans les fichiers de configuration et non dans le code C#. Le code C# de la couche [A] prend alors la forme suivante :

Cacher Sélectionnez
  1. IB ib; // initialisé par Spring
  2. ..
  3. ib.getData(...);
  • ligne 1 : une instance [ib] implémentant l'interface [IB] de la couche [B]. Cette instance est créée par Spring sur la base d'informations trouvées dans un fichier de configuration. Spring va s'occuper de créer :
  • l'instance [b] implémentant la couche [B]
  • l'instance [a] implémentant la couche [A]. Cette instance sera initialisée. Le champ [ib] ci-dessus recevra pour valeur la référence [b] de l'objet implémentant la couche [B]
  • ligne 3 : des données sont demandées à l'instance [ib]

On voit maintenant que, la classe d'implémentation [B1] de la couche B n'apparaît nulle part dans le code de la couche [A]. Lorsque l'implémentation [B1] sera remplacée par une nouvelle implémentation [B2], rien ne changera dans le code de la classe [A]. On changera simplement les fichiers de configuration de Spring pour instancier [B2] au lieu de [B1].

Le couple Spring et interfaces C# apporte une amélioration décisive à la maintenance d'applications en rendant les couches de celles-ci étanches entre elles. C'est cette solution que nous utiliserons pour une nouvelle version de l'application [Impots].

Revenons à l'architecture trois couches de notre application :

Image non disponible

Dans les cas simples, on peut partir de la couche [metier] pour découvrir les interfaces de l'application. Pour travailler, elle a besoin de données :

  • déjà disponibles dans des fichiers, bases de données ou via le réseau. Elles sont fournies par la couche [dao].
  • pas encore disponibles. Elles sont alors fournies par la couche [ui] qui les obtient auprès de l'utilisateur de l'application.

Quelle interface doit offrir la couche [dao] à la couche [metier] ? Quelles sont les interactions possibles entre ces deux couches ? La couche [dao] doit fournir les données suivantes à la couche [metier] :

  • les tranches d'impôt

Dans notre application, la couche [dao] exploite des données existantes mais n'en crée pas de nouvelles. Une définition de l'interface de la couche [dao] pourrait être la suivante :

Cacher Sélectionnez
  1. using Entites;
  2.
  3. namespace Dao {
  4.     public interface IImpotDao {
  5.         // les tranches d'impôt
  6.         TrancheImpot[] TranchesImpot{get;}
  7.     }
  8. }
  • ligne 3 : la couche [dao] sera placée dans l'espace de noms [Dao]
  • ligne 6 : l'interface IImpotDao définit la propriété TranchesImpot qui fournira les tranches d'impôt à la couche [métier].
  • ligne 1 : importe l'espace de noms dans lequel est définie la structure TrancheImpot :
Cacher Sélectionnez
  1. namespace Entites {
  2.     // une tranche d'impôt
  3.     public struct TrancheImpot {
  4.         public decimal Limite { get; set; }
  5.         public decimal CoeffR { get; set; }
  6.         public decimal CoeffN { get; set; }
  7.     }
  8. }

Revenons à l'architecture trois couches de notre application :

Image non disponible

Quelle interface la couche [metier] doit-elle présenter à la couche [ui] ? Rappelons les interactions entre ces deux couches :

  • la couche [ui] demandee à l'utilisateur son nombre d'enfants, son statut marital et son salaire annuel. C'est l'opération [1] ci-dessus.
  • ceci fait, la couche [ui] va demander à la couche métier de faire le calcul des sièges. Pour cela elle va lui transmettre les données qu'elle a reçues de l'utilisateur. C'est l'opération [2].

Une définition de l'interface de la couche [metier] pourrait être la suivante :

Cacher Sélectionnez
  1. namespace Metier {
  2.     interface IImpotMetier {
  3.         int CalculerImpot(bool marié, int nbEnfants, int salaire);
  4.     }
  5. }
  • ligne 1 : on mettra tout ce qui concerne la couche [metier] dans l'espace de noms [Metier].
  • ligne 2 : l'interface IImpotMetier ne définit qu'une méthode : celles qui permet de calculer l'impôt d'un contribuable à partir de son état marital, son nombre d'enfants et son salaire annuel.

Nous étudions une première implémentation de cette architecture en couches.

VII-C. Application exemple - version 4

VII-C-1. Le projet Visual Studio

Le projet Visual Studio sera le suivant :

Image non disponible
  • [1] : le dossier [Entites] contient les objets transversaux aux couches [ui, metier, dao] : la structure TrancheImpot , l'exception FileImpotException .
  • [2] : le dossier [Dao] contient les classes et interfaces de la couche [dao]. Nous utiliserons deux implémentations de l'interface IImpotDao : la classe HardwiredImpot étudiée au paragraphe page et FileImpot étudiée au paragraphe page .
  • [3] : le dossier [Metier] contient les classes et interfaces de la couche [metier]
  • [4] : le dossier [Ui] contient les classes de la couche [ui]
  • [5] : le fichier [DataImpot.txt] contient les tranches d'impôt utilisées par l'implémentation FileImpot de la couche [dao]. Il est configuré [6] pour être automatiquement recopié dans le dossier d'exécution du projet.

VII-C-2. Les entités de l'application

Revenons sur l'architecture 3 couches de notre application :

Image non disponible

Nous appelons entités les classes transversales aux couches. C'est le cas en général des classes et structures qui encapsulent des données de la couche [dao]. Ces entités remontent en général jusqu'à la couche [ui].

Les entités de l'application sont les suivantes :

La structure TrancheImpot

Cacher Sélectionnez
  1. namespace Entites {
  2.     // une tranche d'impôt
  3.     public struct TrancheImpot {
  4.         public decimal Limite { get; set; }
  5.         public decimal CoeffR { get; set; }
  6.         public decimal CoeffN { get; set; }
  7.     }
  8. }

L ' exception FileImpotException

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Entites {
  4.     public class FileImpotException : Exception {
  5.         // codes d'erreur
  6.         [Flags]
  7.         public enum CodeErreurs { Acces = 1, Ligne = 2, Champ1 = 4, Champ2 = 8, Champ3 = 16 };
  8.
  9.         // code d'erreur
  10.         public CodeErreurs Code { get; set; }
  11.
  12.         // constructeurs
  13.         public FileImpotException() {
  14.         }
  15.         public FileImpotException(string message)
  16.             : base(message) {
  17.         }
  18.         public FileImpotException(string message, Exception e)
  19.             : base(message, e) {
  20.         }
  21.     }
  22. }

Note : la classe FileImpotException n'est utile que si la couche [dao] est implémentée par la classe FileImpot .

VII-C-3. La couche [dao]

Image non disponible

Rappelons l'interface de la couche [dao] :

Cacher Sélectionnez
  1. using Entites;
  2.
  3. namespace Dao {
  4.     public interface IImpotDao {
  5.         // les tranches d'impôt
  6.         TrancheImpot[] TranchesImpot{get;}
  7.     }
  8. }

Nous implémenterons cette interface de deux façons différentes.

Tout d'abord avec la classe HardwiredImpot étudiée au paragraphe page :

Cacher Sélectionnez
  1. using System;
  2. using Entites;
  3.
  4. namespace Dao {
  5.     public class HardwiredImpot : IImpotDao {
  6.
  7.         // tableaux de données nécessaires au calcul de l'impôt
  8.         decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
  9.         decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
  10.         decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };
  11.         // tranches d'impôt
  12.         public TrancheImpot[] TranchesImpot { get; private set; }
  13.
  14.         // constructeur
  15.         public HardwiredImpot() {
  16.             // création du tableau des tranches d'impôt
  17.             TranchesImpot = new TrancheImpot[limites.Length];
  18.             // remplissage
  19.             for (int i = 0; i < TranchesImpot.Length; i++) {
  20.                 TranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
  21.             }
  22.         }
  23.     }// classe
  24. }// namespace
  • ligne 5 : la classe HardwiredImpot implémente l'interface IImpotDao
  • ligne 12 : implémentation de la propriété TranchesImpot de l'interface IImpotDao. Cette propriété est une propriété automatique. Elle implémente la méthode get de la propriété TranchesImpot de l'interface IImpotDao. On a de plus déclaré une méthode set privée donc interne à la classe afin que le constructeur des lignes 15-22 puisse initialiser le tableau des tranches d'impôt.

L'interface IImpotDao sera également implémentée par la classe FileImpot étudiée au paragraphe page :

Cacher Sélectionnez
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Text.RegularExpressions;
  5. using Entites;
  6.
  7. namespace Dao {
  8.     class FileImpot : IImpotDao {
  9.
  10.         // fichier des données
  11.         public string FileName { get; set; }
  12.
  13.         // tranches d'impôt
  14.         public TrancheImpot[] TranchesImpot { get; private set; }
  15.
  16.         // constructeur
  17.         public FileImpot(string fileName) {
  18.             // on mémorise le nom du fichier
  19.             FileName = fileName;
  20.             // données
  21.             List<TrancheImpot> listTranchesImpot = new List<TrancheImpot>();
  22.             int numLigne = 1;
  23.             // exception
  24.             FileImpotException fe = null;
  25.             // lecture contenu du fichier fileName, ligne par ligne
  26.             Regex pattern = new Regex(@"s*:\s*");
  27.             // au départ pas d'erreur
  28.             FileImpotException.CodeErreurs code = 0;
  29.             try {
  30.                 using (StreamReader input = new StreamReader(FileName)) {
  31.                     while (!input.EndOfStream && code == 0) {
  32.                         // ligne courante
  33.                         string ligne = input.ReadLine().Trim();
  34.                         // on ignore les lignes vides
  35.                         if (ligne == "")
  36.                             continue;
  37.                         // ligne décomposée en trois champs séparés par :
  38.                         string[] champsLigne = pattern.Split(ligne);
  39.                         // a-t-on 3 champs ?
  40.                         if (champsLigne.Length != 3) {
  41.                             code = FileImpotException.CodeErreurs.Ligne;
  42.                         }
  43.                         // conversions des 3 champs
  44.                         decimal limite = 0, coeffR = 0, coeffN = 0;
  45.                         if (code == 0) {
  46.                             if (!Decimal.TryParse(champsLigne[0], out limite))
  47.                                 code = FileImpotException.CodeErreurs.Champ1;
  48.                             if (!Decimal.TryParse(champsLigne[1], out coeffR))
  49.                                 code |= FileImpotException.CodeErreurs.Champ2;
  50.                             if (!Decimal.TryParse(champsLigne[2], out coeffN))
  51.                                 code |= FileImpotException.CodeErreurs.Champ3;
  52.                             ;
  53.                         }
  54.                         // erreur ?
  55.                         if (code != 0) {
  56.                             // on note l'erreur
  57.                             fe = new FileImpotException(String.Format("Ligne n° {0} incorrecte", numLigne)) { Code = code };
  58.                         } else {
  59.                             // on mémorise la nouvelle tranche d'impôt
  60.                             listTranchesImpot.Add(new TrancheImpot() { Limite = limite, CoeffR = coeffR, CoeffN = coeffN });
  61.                             // ligne suivante
  62.                             numLigne++;
  63.                         }
  64.                     }
  65.                 }
  66.             } catch (Exception e) {
  67.                 // on note l'erreur
  68.                 fe = new FileImpotException(String.Format("Erreur lors de la lecture du fichier {0}", FileName), e) { Code = FileImpotException.CodeErreurs.Acces };
  69.             }
  70.             // erreur à signaler ?
  71.             if (fe != null) {
  72.                 // on lance l'exception
  73.                 throw fe;
  74.             } else {
  75.                 // on rend la liste listImpot dans le tableau tranchesImpot
  76.                 TranchesImpot = listTranchesImpot.ToArray();
  77.             }
  78.         }
  79.     }
  80. }
  • ce code a déjà été étudié au paragraphe page .
  • ligne 14 : la méthode TranchesImpot de l'interface IImpotDao
  • ligne 76 : initialisation des tranches d'impôt dans le constructeur de la classe, à partir du fichier dont le contructeur a reçu le nom ligne 17.

VII-C-4. La couche [metier]

Image non disponible

Rappelons l'interface de cette couche :

Cacher Sélectionnez
  1. namespace Metier {
  2.     public interface IImpotMetier {
  3.         int CalculerImpot(bool marié, int nbEnfants, int salaire);
  4.     }
  5. }

L'implémentation ImpotMetier de cette interface est la suivante :

Cacher Sélectionnez
  1. using Entites;
  2. using Dao;
  3.
  4. namespace Metier {
  5.     public class ImpotMetier : IImpotMetier {
  6.
  7.         // couche [dao]
  8.         private IImpotDao Dao { get; set; }
  9.
  10.         // les tranches d'impôt
  11.         private TrancheImpot[] tranchesImpot;
  12.
  13.         // constructeur
  14.         public ImpotMetier(IImpotDao dao) {
  15.             // mémorisation
  16.             Dao = dao;
  17.             // tranches d'impôt
  18.             tranchesImpot = dao.TranchesImpot;
  19.         }
  20.
  21.         // calcul de l'impôt
  22.         public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
  23.             // calcul du nombre de parts
  24.             decimal nbParts;
  25.             if (marié)
  26.                 nbParts = (decimal)nbEnfants / 2 + 2;
  27.             else
  28.                 nbParts = (decimal)nbEnfants / 2 + 1;
  29.             if (nbEnfants >= 3)
  30.                 nbParts += 0.5M;
  31.             // calcul revenu imposable & Quotient familial
  32.             decimal revenu = 0.72M * salaire;
  33.             decimal QF = revenu / nbParts;
  34.             // calcul de l'impôt
  35.             tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
  36.             int i = 0;
  37.             while (QF > tranchesImpot[i].Limite)
  38.                 i++;
  39.             // retour résultat
  40.             return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
  41.         }//calculer
  42.     }//classe
  43.
  44. }
  • ligne 5 : la classe [Metier] implémente l'interface [IImpotMetier].
  • lignes 14-19 : la couche [metier] doit collaborer avec la couche [dao]. Elle doit donc avoir une référence sur l'objet implémentant l'interface IImpotDao . C'est pourquoi cette reférence est-elle passée en paramètre au constructeur.
  • ligne 16 : la référence sur la couche [dao] est mémorisé dans le champ privé de la ligne 8
  • ligne 18 : à partir de cette référence, le constructeur demande le tableau des tranches d'impôt et en mémorise une référence dans la propriété privée de la ligne 8.
  • lignes 22-41 : implémentation de la méthode CalculerImpot de l'interface IImpotMetier . Cette implémentation utilise le tableau des tranches d'impôt initialisé par le constructeur.

VII-C-5. La couche [ui]

Image non disponible

Les classes de dialogue avec l'utilisateur des versions 2 et 3 étaient très proches. Celle de la version 2 était la suivante :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap2 {
  4.     public class Program {
  5.         static void Main() {
  6. ...
  7.
  8.             // création d'un objet IImpot
  9.             IImpot impot = new HardwiredImpot();
  10.
  11.             // boucle infinie
  12.             while (true) {
  13. ...
  14.             }//while
  15.         }
  16.     }
  17. }

et celle de la version 3 :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap3 {
  4.     public class Program {
  5.         static void Main() {
  6. ...
  7.
  8.             // création d'un objet IImpot
  9.             IImpot impot = null;
  10.             try {
  11.                 // création d'un objet IImpot
  12.                 impot = new FileImpot("DataImpotInvalide.txt");
  13.             } catch (FileImpotException e) {
  14.                 // affichage erreur
  15.                 string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
  16.                 Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
  17.                 // arrêt programme
  18.                 Environment.Exit(1);
  19.             }
  20.             // boucle infinie
  21.             while (true) {
  22. ...
  23.             }//while
  24.         }
  25.     }
  26. }

Seule change la façon d'instancier l'objet de type IImpot qui permet le calcul de l'impôt. Cet objet correspond ici à notre couche [métier].

Pour une implémentation [dao] avec la classe HardwiredImpot , la classe de dialogue est la suivante :

Cacher Sélectionnez
  1. using System;
  2. using Metier;
  3. using Dao;
  4. using Entites;
  5.
  6. namespace Ui {
  7.     public class Dialogue2 {
  8.         static void Main() {
  9. ...
  10.
  11.             // on crée les couches [metier et dao]
  12.             IImpotMetier metier = new ImpotMetier(new HardwiredImpot());
  13.
  14.             // boucle infinie
  15.             while (true) {
  16. ...
  17.                 // les paramètres sont corrects - on calcule l'Impot
  18.                 Console.WriteLine("Impot=" + metier.CalculerImpot(marié == "o", nbEnfants, salaire) + " euros");
  19.                 // contribuable suivant
  20.             }//while
  21.         }
  22.     }
  23. }
  • ligne 12 : instanciation des couches [dao] et [metier]. On rappelle que la couche [metier] a besoin de la couche [dao].
  • ligne 18 : utilisation de la couche [metier] pour calculer l'impôt

Pour une implémentation [dao] avec la classe FileImpot , la classe de dialogue est la suivante :

Cacher Sélectionnez
  1. using System;
  2. using Metier;
  3. using Dao;
  4. using Entites;
  5.
  6. namespace Ui {
  7.     public class Dialogue {
  8.         static void Main() {
  9. ...
  10.             // on crée les couches [metier et dao]
  11.             IImpotMetier metier = null;
  12.             try {
  13.         // création couche [metier]
  14.                 metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
  15.             } catch (FileImpotException e) {
  16.                 // affichage erreur
  17.                 string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
  18.                 Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
  19.                 // arrêt programme
  20.                 Environment.Exit(1);
  21.             }
  22.             // boucle infinie
  23.             while (true) {
  24. ...
  25.                 // les paramètres sont corrects - on calcule l'Impot
  26.                 Console.WriteLine("Impot=" + metier.CalculerImpot(marié == "o", nbEnfants, salaire) + " euros");
  27.                 // contribuable suivant
  28.             }//while
  29.         }
  30.     }
  31. }
  • ligne 11-21 : instanciation des couches [dao] et [metier]. L'instanciation de la couche [dao] pouvant lancer une exception, celle-ci est gérée
  • ligne 26 : utilisation de la couche [metier] pour calculer l'impôt, comme dans la version précédente

VII-C-6. Conclusion

L'architecture en couches et l'utilisation d'interfaces a amené une certaine souplesse à notre application. Celle-ci apparaît notamment dans la façon dont la couche [ui] instancie les couches [dao] et [métier] :

Cacher Sélectionnez
  1.         // on crée les couches [metier et dao]
  2. IImpotMetier metier = new ImpotMetier(new HardwiredImpot());

dans un cas et :

Cacher Sélectionnez
  1. // on crée les couches [metier et dao]
  2.             IImpotMetier metier = null;
  3.             try {
  4.         // création couche [metier]
  5.                 metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
  6.             } catch (FileImpotException e) {
  7.                 // affichage erreur
  8.                 string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
  9.                 Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
  10.                 // arrêt programme
  11.                 Environment.Exit(1);
  12.             }

dans l'autre. Si on excepte la gestion de l'exception dans le cas 2, l'instanciation des couches [dao] et [metier] est similaire dans les deux applications. Une fois les couches [dao] et [metier] instanciées, le code de la couche [ui] est identique dans les deux cas. Ceci est du au fait que la couche [métier] est manipulée via son interface IImpotMetier et non via la classe d'implémentation de celle-ci. Changer la couche [metier] ou la couche [dao] de l'application sans changer leurs interfaces reviendra toujours à changer les seules lignes précédentes dans la couche [ui].

Un autre exemple de souplesse amenée par cette architecture est celui de l'implémentation de la couche [métier] :

Cacher Sélectionnez
  1. using Entites;
  2. using Dao;
  3.
  4. namespace Metier {
  5.     public class ImpotMetier : IImpotMetier {
  6.
  7.         // couche [dao]
  8.         private IImpotDao Dao { get; set; }
  9.
  10.         // les tranches d'impôt
  11.         private TrancheImpot[] tranchesImpot;
  12.
  13.         // constructeur
  14.         public ImpotMetier(IImpotDao dao) {
  15.             // mémorisation
  16.             Dao = dao;
  17.             // tranches d'impôt
  18.             tranchesImpot = dao.TranchesImpot;
  19.         }
  20.
  21.         // calcul de l'impôt
  22.         public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
  23. ...
  24.         }//calculer
  25.     }//classe
  26.
  27. }

Ligne 14, on voit que la couche [métier] est construite à partir d'une référence sur l'interface de la couche [dao]. Changer l'implémentation de cette dernière a donc un impact zéro sur la couche [métier]. C'est pour cela, que notre unique implémentation de la couche [métier] a pu travailler sans modifications avec deux implémentations différentes de la couche [dao].

VII-D. Application exemple - version 5

Image non disponible

Cette nouvelle version reprend la précédente en y apportant les modifications suivantes :

  • les couches [métier] et [dao] sont chacune encapsulées dans une DLL et testée avec le framework de tests unitaires NUnit.
  • l'intégration des couches est assurée par le framework Spring

Dans les grands projets, plusieurs développeurs travaillent sur le même projet. Les architectures en couches facilitent ce mode de travail : parce que les couches communiquent entre-elles avec des interfaces bien définies, un développeurs travaillant sur une couche n'a pas à se préoccuper du travail des autres développeurs sur les autres couches. Il suffit juste que tout le monde respecte les interfaces.

Ci-dessus, le développeur de la couche [métier] aura besoin au moment des tests de sa couche d'une implémentation de la couche [dao]. Tant que celle-ci n'est pas terminée, il peut utiliser une implémentation factice de la couche [dao] tant qu'elle respecte l'interface IImpotDao . C'est là également un avantage de l'architecture en couches : un retard dans la couche [dao] n'empêche pas les tests de la couche [métier]. L'implémentation factice de la couche [dao] a également l'avantage d'être bien souvent plus facile à mettre en œuvre que la véritable couche [dao] qui peut nécessiter de lancer un SGBD, d'avoir des connexions réseau…

Lorsque la couche [dao] est terminée et testée, elle sera fournie aux développeurs de la couche [métier] sous la forme d'une DLL plutôt que de code source. Au final, l'application est souvent délivrée sous la forme d'un exécutable .exe (celui de la couche [ui]) et de bibliothèques de classes .dll (les autres couches).

VII-D-1. NUnit

Les tests faits jusqu'à maintenant pour nos diverses applications reposaient sur une vérification visuelle. On vérifiait qu'on obtenait à l'écran ce qui était attendu. C'est une méthode inutilisable lorsqu'il y a de nombreux tests à faire. L'être humain est en effet sujet à la fatigue et sa capacité à vérifier des tests s'émousse au fil de la journée. Les tests doivent alors être automatisés et viser à ne nécessiter aucune intervention humaine.

Une application évolue au fil du temps. A chaque évolution, on doit vérifier que l'application ne "régresse" pas, c.a.d. qu'elle continue à passer les tests de bon fonctionnement qui avaient été faits lors de son écriture initiale. On appelle ces tests, des tests de "non régression". Une application un peu importante peut nécessiter des centaines de tests. On teste en effet chaque méthode de chaque classe de l'application. On appelle cela des tests unitaires. Ceux-ci peuvent mobiliser beaucoup de développeurs s'ils n'ont pas été automatisés.

Des outils ont été développés pour automatiser les tests. L'un d'eux s'appelle NUnit. Il est disponible sur le site [http://www.nunit.org] :

Image non disponible

C'est la version 2.4.6 ci-dessus qui a été utilisée pour ce document (mars 2008). L'installation place une icône [1] sur le bureau :

Image non disponible

Un double-clic sur l'icône [1] lance l'interface graphique de NUnit [2]. Celle-ci n'aide en rien à l'automatisation des tests puisque de nouveau nous sommes ramenés à une vérification visuelle : le testeur vérifie les résultats des tests affichés dans l'interface graphique. Néanmoins les tests peuvent être également exécutés par des outils batch et leurs résultats enregistrés dans des fichiers XML. C'est cette méthode qui est utilisée par les équipes de développement : les tests sont lancés la nuit et les développeurs ont le résultat le lendemain matin.

Examinons avec un exemple le principe des tests NUnit. Tout d'abord, créons un nouveau projet C# de type Console Application :

Image non disponible

En [1], on voit les références du projet. Ces références sont des DLL contenant des classes et interfaces utilisées par le projet. Celles présentées en [1] sont incluses par défaut dans chaque nouveau projet C#. Pour pouvoir utiliser les classes et interfaces du framework NUnit, il nous faut ajouter [2] une nouvelle référence au projet.

Image non disponible

Dans l'onglet .NET ci-dessus, nous choisissons le composant [nunit.framework]. Les composants [nunit.*] ci-dessus ne sont pas des composants présents par défaut dans l'environnement .NET. Ils y ont été amenés par l'installation précédente du framework NUnit. Une fois l'ajout de la référence validée, celle-ci apparaît [4] dans la liste des références du projet.

Avant génération de l'application, le dossier [bin/Release] du projet est vide. Après génération (F6), on peut constater que le dossier [bin/Release] n'est plus vide :

Image non disponible

En [6], on voit la présence de la DLL [nunit.framework.dll]. C'est l'ajout de la référence [nunit.framework] qui a provoqué la copie de cette DLL dans le dossier d'exécution. Celui-ci est en effet l'un des dossiers qui seront explorés par le CLR (Common Language Runtime) .NET pour trouver les classes et interfaces référencées par le projet.

Construisons une première classe de test NUnit. Pour cela, nous supprimons la classe [Program.cs] générée par défaut puis nous ajoutons une nouvelle classe [Nunit1.cs] au projet. Nous supprimons également les références inutiles [7].

La classe de test NUnit1 sera la suivante :

Cacher Sélectionnez
  1. using System;
  2. using NUnit.Framework;
  3.
  4. namespace NUnit {
  5.     [TestFixture]
  6.     public class NUnit1 {
  7.         public NUnit1() {
  8.             Console.WriteLine("constructeur");
  9.         }
  10.         [SetUp]
  11.         public void avant() {
  12.             Console.WriteLine("Setup");
  13.         }
  14.         [TearDown]
  15.         public void après() {
  16.             Console.WriteLine("TearDown");
  17.         }
  18.         [Test]
  19.         public void t1() {
  20.             Console.WriteLine("test1");
  21.             Assert.AreEqual(1, 1);
  22.         }
  23.         [Test]
  24.         public void t2() {
  25.             Console.WriteLine("test2");
  26.             Assert.AreEqual(1, 2, "1 n'est pas égal à 2");
  27.         }
  28.     }
  29. }
  • ligne 6 : la classe NUnit1 doit être publique. Le mot clé public n'est pas généré par défaut par Visual Studio. Il faut le rajouter.
  • ligne 5 : l'attribut [ TestFixture ] est un attribut NUnit . Il indique que la classe est une classe de test.
  • lignes 7-9 : le constructeur. Il n'est utilisé ici que pour écrire un message à l'écran. On veut voir quand il est exécuté.
  • ligne 10 : l'attribut [ SetUp ] définit une méthode exécutée avant chaque test unitaire.
  • ligne 14 : l'attribut [ TearDown ] définit une méthode exécuté après chaque test unitaire.
  • ligne 18 : l'attribut [ Test ] définit une méthode de test. Pour chaque méthode annotée avec l'attribut [ Test ], la méthode annotée [ SetUp ] sera exécutée avant le test et la méthode annotée [ TearDown ] sera exécutée après le test.
  • ligne 21 : l'une des méthodes [ Assert.* ] définies par le framework NUnit. On trouve les méthodes [Assert] suivantes :
  • [ Assert.AreEqual(expression1, expression2) ] : vérifie que les valeurs des deux expressions sont égales. De nombreux types d'expression sont acceptés (int, string, float, double, decimal…). Si les deux expressions ne sont pas égales, alors une exception est lancée.
  • [ Assert.AreEqual(réel1, réel2, delta) ] : vérifie que deux réels sont égaux à delta près, c.a.d abs(réel1-réel2)<=delta. On pourra écrire par exemple [ Assert.AreEqual(réel1, réel2, 1E-6) ] pour vérifier que deux valeurs sont égales à 10-6 près.
  • [ Assert.AreEqual(expression1, expression2, message) ] et [ Assert.AreEqual(réel1, réel2, delta, message) ] sont des variantes permettant de préciser le message d'erreur à associer à l'exception lancée lorsque la méthode [ Assert.AreEqual ] échoue.
  • [ Assert.IsNotNull(object) ] et [ Assert.IsNotNull(object, message) ] : vérifie que object n'est pas égal à null.
  • [ Assert.IsNull(object) ] et [ Assert.IsNull(object, message) ] : vérifie que object est égal à null.
  • [ Assert.IsTrue(expression) ] et [ Assert.IsTrue(expression, message) ] : vérifie que expression est égale à true.
  • [ Assert.IsFalse(expression) ] et [ Assert.IsFalse(expression, message) ] : vérifie que expression est égale à false.
  • [ Assert.AreSame(object1, object2) ] et [ Assert.AreSame(object1, object2, message) ] : vérifie que les références object1 et object2 désignent le même objet.
  • [ Assert.AreNotSame(object1, object2) ] et [ Assert.AreNotSame(object1, object2, message) ] : vérifie que les références object1 et object2 ne désignent pas le même objet.
  • ligne 21 : l'assertion doit réussir
  • ligne 26 : l'assertion doit échouer

Configurons le projet afin que sa génération produise une DLL plutôt qu'un exécutable .exe :

Image non disponible
  • en [1] : propriétés du projet
  • en [2, 3] : comme type de projet, on choisit [Class Library] (Bibliothèque de classes)
  • en [4] : la génération du projet produira une DLL (assembly) appelée [Nunit.dll]

Utilisons maintenant, NUnit pour exécuter la classe de test :

Image non disponible
  • en [1] : ouverture d'un projet NUnit
  • en [2, 3] : on charge la DLL bin/Release/Nunit.dll produite par la génération du projet C#
  • en [4] : la DLL a été chargée
  • en [5] : l'arbre des tests
  • en [6] : on les exécute
Image non disponible
  • en [7] : les résultats : t1 a réussi, t2 a échoué
  • en [8] : une barre rouge indique l'échec global de la classe de tests
  • en [9] : le message d'erreur lié au test raté
Image non disponible
  • en [11] : les différents onglets de la fenêtre des résultats
  • en [12] : l'onglet [Console.Out]. On y voit que :
  • le constructeur n'a été exécuté qu'une fois
  • la méthode [SetUp] a été exécutée avant chacun des deux tests
  • la méthode [TearDown] a été exécutée après chacun des deux tests

Il est possible de préciser les méthodes à tester :

Image non disponible
  • en [1] : on demande l'affichage d'une case à cocher à côté de chaque test
  • en [2] : on coche les tests à exécuter
  • en [3] : on les exécute

Pour corriger les erreurs, il suffit de corriger le projet C# et de le régénérer. NUnit détecte que la DLL qu'il teste a été changée et charge la nouvelle automatiquement. Il suffit alors de relancer les tests.

Considérons la nouvelle classe de test suivante :

Cacher Sélectionnez
  1. using System;
  2. using NUnit.Framework;
  3.
  4. namespace NUnit {
  5.     [TestFixture]
  6.     public class NUnit2 : AssertionHelper {
  7.         public NUnit2() {
  8.             Console.WriteLine("constructeur");
  9.         }
  10.         [SetUp]
  11.         public void avant() {
  12.             Console.WriteLine("Setup");
  13.         }
  14.         [TearDown]
  15.         public void après() {
  16.             Console.WriteLine("TearDown");
  17.         }
  18.         [Test]
  19.         public void t1() {
  20.             Console.WriteLine("test1");
  21.             Expect(1, EqualTo(1));
  22.         }
  23.         [Test]
  24.         public void t2() {
  25.             Console.WriteLine("test2");
  26.             Expect(1, EqualTo(2), "1 n'est pas égal à 2");
  27.         }
  28.     }
  29. }

A partir de la version 2.4 de NUnit, une nouvelle syntaxe est devenue disponible, celles des lignes 21 et 26. Pour cela, la classe de test doit dériver de la classe AssertionHelper (ligne 6).

La correspondance (non exhaustive) entre ancienne et nouvelle syntaxe est la suivante :

Assert.AreEqual(expression1, expression2, message) | Expect(expression1,EqualTo(expression2),message)
Assert.AreEqual(réel1, réel2, delta, message) | Expect(expression1,EqualTo(expression2).Within(delta),message)
Assert.AreSame(objet1, objet2, message) | Expect(objet1,SameAs(objet2),message)
Assert.AreNotSame(objet1, objet2, message) | Expect(objet1,Not.SameAs(objet2),message)
Assert.IsNull(objet,message) | Expect(objet,Null,message)
Assert.IsNotNull(objet,message) | Expect(objet,Not.Null,message)
Assert.IsTrue(expression,message) | Expect(expression,True,message)
Assert.IsFalse(expression,message) | Expect(expression,False,message)

Ajoutons le test suivant à la classe NUnit2 :

Cacher Sélectionnez
  1.         [Test]
  2.         public void t3() {
  3.             bool vrai = true, faux = false;
  4.             Expect(vrai, True);
  5.             Expect(faux, False);
  6.             Object obj1 = new Object(), obj2 = null, obj3=obj1;
  7.             Expect(obj1, Not.Null);
  8.             Expect(obj2, Null);
  9.             Expect(obj3, SameAs(obj1));
  10.             double d1 = 4.1, d2 = 6.4, d3 = d1;
  11.             Expect(d1, EqualTo(d3).Within(1e-6));
  12.             Expect(d1, Not.EqualTo(d2));
  13. }

Si on génère (F6) la nouvelle DLL du projet C#, le projet NUnit devient le suivant :

Image non disponible
  • en [1] : la nouvelle classe de test [NUnit2] a été automatiquement détectée
  • en [2] : on exécute le test t3 de NUnit2
  • en [3] : le test t3 a été réussi

Pour approfondir NUnit, on lira l'aide de NUnit :

Image non disponible

VII-D-2. La solution Visual Studio

Image non disponible

Nous allons construire progressivement la solution Visual Studio suivante :

Image non disponible
  • en [1] : la solution ImpotsV5 est formée de trois projets, un pour chacune des trois couches de l'application
  • en [2] : le projet [dao] de la couche [dao]
  • en [3] : le projet [metier] de la couche [metier]
  • en [4] : le projet [ui] de la couche [ui]

La solution ImpotsV5 peut être construite de la façon suivante :

Image non disponible
  • en [1] : créer un nouveau projet
  • en [2] : choisir une application console
  • en [3] : appeler le projet [dao]
  • en [4] : créer le projet
  • en [5] : une fois le projet créé, le sauvegarder
Image non disponible
  • en [6] : garder le nom [dao] pour le projet
  • en [7] : préciser un dossier pour enregistrer le projet et sa solution
  • en [8] : donner un nom à la solution
  • en [9] : indiquer que la solution doit avoir son propre dossier
  • en [10] : enregistrer le projet et sa solution
  • en [11] : le projet [dao] dans sa solution ImpotsV5
Image non disponible
  • en [12] : le dossier de la solution ImpotsV5 . Il contient le dossier [dao] du dossier [dao].
  • en [13] : le contenu du dossier [dao]
  • en [14] : on ajoute un nouveau projet à la solution ImpotsV5
Image non disponible
  • en [15] : le nouveau projet s'appelle [metier]
  • en [16] : la solution avec ses deux projets
  • en [17] : la solution, une fois qu'on lui a ajouté le 3ième projet [ui]
Image non disponible
  • en [18] : le dossier de la solution et les dossiers des trois projets
  • lorsqu'on exécute une solution par (Ctrl+F5), c'est le projet actif qui est exécuté. Il en est de même lorsqu'on génère (F6) la solution. Le nom du project actif est en gras [19] dans la solution.
  • en [20] : pour changer le projet actif de la solution
  • en [21] : le projet [metier] est désormais le projet actif de la solution

VII-D-3. La couche [dao]

Image non disponible
Image non disponible

Les références du projet (cf [1] dans le projet)

On ajoute la référence [nunit.framework] nécessaire aux tests [NUnit]

Les entités (cf [2] dans le projet)

La classe [TrancheImpot] est celle des versions précédentes. La classe [FileImpotException] de la version précédente est renommée en [ImpotException] pour la rendre plus générique et ne pas la lier à une couche [dao] particulière :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Entites {
  4.     public class ImpotException : Exception {
  5.
  6.         // code d'erreur
  7.         public int Code { get; set; }
  8.
  9.         // constructeurs
  10.         public ImpotException() {
  11.         }
  12.         public ImpotException(string message)
  13.             : base(message) {
  14.         }
  15.         public ImpotException(string message, Exception e)
  16.             : base(message, e) {
  17.         }
  18.     }
  19. }

La couche [dao] (cf [3] dans le projet)

L'interface [IImpotDao] est celle de la version précédente. Il en est de même pour la classe [HardwiredImpot]. La classe [FileImpot] évolue pour tenir compte du changement de l'exception [FileImpotException] en [ImpotException] :

Cacher Sélectionnez
  1. ...
  2.
  3. namespace Dao {
  4.     public class FileImpot : IImpotDao {
  5.
  6.         // codes d'erreur
  7.         [Flags]
  8.         public enum CodeErreurs { Acces = 1, Ligne = 2, Champ1 = 4, Champ2 = 8, Champ3 = 16 };
  9.
  10. ...
  11.
  12.         // constructeur
  13.         public FileImpot(string fileName) {
  14.             // on mémorise le nom du fichier
  15.             FileName = fileName;
  16. ...
  17.             // au départ pas d'erreur
  18.             CodeErreurs code = 0;
  19.             try {
  20.                 using (StreamReader input = new StreamReader(FileName)) {
  21.                     while (!input.EndOfStream && code == 0) {
  22. ...
  23.                         // erreur ?
  24.                         if (code != 0) {
  25.                             // on note l'erreur
  26.                             fe = new ImpotException(String.Format("Ligne n° {0} incorrecte", numLigne)) { Code = (int)code };
  27.                         } else {
  28. ...
  29.                         }
  30.                     }
  31.                 }
  32.             } catch (Exception e) {
  33.                 // on note l'erreur
  34.                 fe = new ImpotException(String.Format("Erreur lors de la lecture du fichier {0}", FileName), e) { Code = (int)CodeErreurs.Acces };
  35.             }
  36.             // erreur à signaler ?
  37. ...
  38.         }
  39.     }
  40. }
  • ligne 8 : les codes d'erreurs auparavant dans la classe [FileImpotException] ont migré dans la classe [FileImpot]. Ce sont en effet des codes d'erreur spécifiques à cette implémentation de l'interface [IImpotDao].
  • lignes 26 et 34 : pour encapsuler une erreur, c'est la classe [ImpotException] qui est utilisée et non plus la classe [FileImpotException].

Le test [Test1] (cf [4] dans le projet)

La classe [Test1] se contente d'afficher les tranches d'impôt à l'écran :

Cacher Sélectionnez
  1. using System;
  2. using Dao;
  3. using Entites;
  4.
  5. namespace Tests {
  6.     class Test1 {
  7.         static void Main() {
  8.
  9.             // on crée la couche [dao]
  10.             IImpotDao dao = null;
  11.             try {
  12.                 // création couche [dao]
  13.                 dao = new FileImpot("DataImpot.txt");
  14.             } catch (ImpotException e) {
  15.                 // affichage erreur
  16.                 string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
  17.                 Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
  18.                 // arrêt programme
  19.                 Environment.Exit(1);
  20.             }
  21.             // on affiche les tranches d'impôt
  22.             TrancheImpot[] tranchesImpot = dao.TranchesImpot;
  23.             foreach (TrancheImpot t in tranchesImpot) {
  24.                 Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
  25.             }
  26.         }
  27.     }
  28. }
  • ligne 13 : la couche [dao] est implémentée par la classe [FileImpot]
  • ligne 14 : on gère l'exception de type [ImpotException] qui peut survenir.

Le fichier [DataImpot.txt] nécessaire aux tests est recopié automatiquement dans le dossier d'exécution du projet (cf [5] dans le projet). Le projet [dao] va avoir plusieurs classes contenant une méthode [Main]. Il faut indiquer alors explicitement la classe à exécuter lorsque l'utilisateur demande l'exécution du project par Ctrl-F5 :

Image non disponible
  • en [1] : accéder aux propriétés du projet
  • en [2] : préciser que c'est une application console
  • en [3] : préciser la classe à exécuter

L'exécution de la classe [Test1] précédente donne les résultats suivants :

Cacher Sélectionnez
  1. 4962:0:0
  2. 8382:0,068:291,09
  3. 14753:0,191:1322,92
  4. 23888:0,283:2668,39
  5. 38868:0,374:4846,98
  6. 47932:0,426:6883,66
  7. 0:0,481:9505,54

Le test [Test2] (cf [4] dans le projet)

La classe [Test2] fait la même chose que la classe [Test1] en implémentant la couche [dao] avec la classe [HardwiredImpot]. La ligne 13 de [Test1] est remplacée par la suivante :

Cacher Sélectionnez
  1.                 dao = new HardwiredImpot();

Le projet est modifié pour exécuter désormais la classe [Test2] :

Image non disponible

Les résultats écran sont les mêmes que précédemment.

Le test NUnit [NUnit1] (cf [4] dans le projet)

Le test unitaire [NUnit1] est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using Dao;
  3. using Entites;
  4. using NUnit.Framework;
  5.
  6. namespace Tests {
  7.     [TestFixture]
  8.     public class NUnit1 : AssertionHelper{
  9.         // couche [dao] à tester
  10.         private IImpotDao dao;
  11.
  12.         // constructeur
  13.         public NUnit1() {
  14.             // initialisation couche [dao]
  15.             dao = new FileImpot("DataImpot.txt");
  16.         }
  17.
  18.         // test
  19.         [Test]
  20.         public void ShowTranchesImpot(){
  21.             // on affiche les tranches d'impôt
  22.             TrancheImpot[] tranchesImpot = dao.TranchesImpot;
  23.             foreach (TrancheImpot t in tranchesImpot) {
  24.                 Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
  25.             }
  26.             // qqs tests
  27.             Expect(tranchesImpot.Length,EqualTo(7));
  28.             Expect(tranchesImpot[2].Limite,EqualTo(14753));
  29.             Expect(tranchesImpot[2].CoeffR, EqualTo(0.191));
  30.             Expect(tranchesImpot[2].CoeffN, EqualTo(1322.92));
  31.         }
  32.     }
  33. }
  • la classe de test dérive de la classe [AssertionHelper], ce qui permet l'utilisation de la méthode statique Expect (lignes 27-30).
  • ligne 10 : une référence sur la couche [dao]
  • lignes 13-16 : le constructeur instancie la couche [dao] avec la classe [FileImpot]
  • lignes 19-20 : la méthode de test
  • ligne 22 : on récupère le tableau des tranches d'impôt auprès de la couche [dao]
  • lignes 23-25 : on les affiche comme précédemment. Cet affichage n'aurait pas lieu d'être dans un test unitaire réel. Ici, cet affichage a un souci pédagogique.
  • lignes 27 : on vérifie qu'il y a bien 7 tranches d'impôt
  • lignes 28-30 : on vérifie les valeurs de la tranche d'impôt n° 2

Pour exécuter ce test unitaire, le projet doit être de type [Class Library] :

Image non disponible
  • en [1] : la nature du projet a été changée
  • en [2] : la DLL générée s'appellera []
  • en [3] : après génération (F6) du projet, le dossier [dao/bin/Release] contient la DLL [ImpotsV5-dao.dll]

La DLL [ImpotsV5-dao.dll] est ensuite chargée dans le framework NUnit et exécutée :

Image non disponible
  • en [1] : les tests ont été réussis. Nous considérons désormais la couche [dao] opérationnelle. Sa DLL contient toutes les classes du projet dont les classes de test. Celles-ci sont inutiles. Nous reconstruisons la DLL afin d'en exclure les classes de tests.
  • en [2] : le dossier [tests] est exclu du projet
  • en [3] : le nouveau projet. Celui-ci est régénéré par F6 afin de générer une nouvelle DLL.

VII-D-4. La couche [metier]

Image non disponible
Image non disponible
  • en [1], le projet [metier] est devenu le projet actif de la solution
  • en [2] : les références du projet
  • en [3] : la couche [metier]
  • en [4] : les classes de test
  • en [5] : le fichier [DataImpot.txt] des tranches d'impôt configuré [6] pour être recopié automatiquement dans le dossier d'exécution du projet [7]

Les références du projet (cf [2] dans le projet)

Comme pour le projet [dao], on ajoute la référence [nunit.framework] nécessaire aux tests [NUnit]. La couche [metier] a besoin de la couche [dao]. Il lui faut donc une référence sur la DLL de cette couche. On procède ainsi :

Image non disponible
  • en [1] : on ajoute une nouvelle référence aux références du projet [metier]
  • en [2] : on sélectionne l'onglet [Browse]
  • en [3] : on sélectionne le dossier [dao/bin/Release]
  • en [4] : on sélectionne la DLL [ImpotsV5-dao.dll] générée dans le projet [dao]
  • en [5] : la nouvelle référence

La couche [metier] (cf [3] dans le projet)

L'interface [IImpotMetier] est celle de la version précédente. Il en est de même pour la classe [ImpotMetier].

Le test [Test1] (cf [4] dans le projet)

La classe [Test1] se contente de faire quelques calculs de salaire :

Cacher Sélectionnez
  1. using System;
  2. using Dao;
  3. using Entites;
  4. using Metier;
  5.
  6. namespace Tests {
  7.     class Test1 {
  8.         static void Main() {
  9.
  10.             // on crée la couche [metier]
  11.             IImpotMetier metier = null;
  12.             try {
  13.                 // création couche [metier]
  14.                 metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
  15.             } catch (ImpotException e) {
  16.                 // affichage erreur
  17.                 string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
  18.                 Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
  19.                 // arrêt programme
  20.                 Environment.Exit(1);
  21.             }
  22.             // on calcule qqs impots
  23.             Console.WriteLine(String.Format("Impot(true,2,60000)={0} euros", metier.CalculerImpot(true, 2, 60000)));
  24.             Console.WriteLine(String.Format("Impot(false,3,60000)={0} euros", metier.CalculerImpot(false, 3, 60000)));
  25.             Console.WriteLine(String.Format("Impot(false,3,60000)={0} euros", metier.CalculerImpot(false, 3, 6000)));
  26.             Console.WriteLine(String.Format("Impot(false,3,60000)={0} euros", metier.CalculerImpot(false, 3, 600000)));
  27.         }
  28.     }
  29. }
  • ligne 14 : création des couches [metier] et [dao]. La couche [dao] est implémentée avec la classe [FileImpot]
  • lignes 12-21 : gestion d'une éventuelle exception de type [ImpotException]
  • lignes 23-26 : appels répétés de l'unique méthode CalculerImpot de l'interface [IImpotMetier].

Le projet [metier] est configuré comme suit :

Image non disponible
  • [1] : le projet est de type application console
  • [2] : la classe exécutée est la classe [Test1]
  • [3] : la génération du projet produira l'exécutable [ImpotsV5-metier.exe]

L'exécution du projet donne les résultats suivants :

  • Impot(true,2,60000)=4282 euros
  • Impot(false,3,60000)=4282 euros
  • Impot(false,3,60000)=0 euros
  • Impot(false,3,60000)=179275 euros

Le test [NUnit1] (cf [4] dans le projet)

La classe de tests unitaires [NUnit1] reprend les quatre calculs précédents et en vérifie le résultat :

Cacher Sélectionnez
  1. using Dao;
  2. using Metier;
  3. using NUnit.Framework;
  4.
  5. namespace Tests {
  6.     [TestFixture]
  7.     public class NUnit1:AssertionHelper {
  8.         // couche [metier] à tester
  9.         private IImpotMetier metier;
  10.
  11.         // constructeur
  12.         public NUnit1() {
  13.             // initialisation couche [metier]
  14.             metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
  15.         }
  16.
  17.         // test
  18.         [Test]
  19.         public void CalculsImpot(){
  20.             // on affiche les tranches d'impôt
  21.             Expect(metier.CalculerImpot(true, 2, 60000), EqualTo(4282));
  22.             Expect(metier.CalculerImpot(false, 3, 60000), EqualTo(4282));
  23.             Expect(metier.CalculerImpot(false, 3, 6000), EqualTo(0));
  24.             Expect(metier.CalculerImpot(false, 3, 600000), EqualTo(179275));
  25.         }
  26.     }
  27. }
  • ligne 14 : création des couches [metier] et [dao]. La couche [dao] est implémentée avec la classe [FileImpot]
  • lignes 21-24 : appels répétés de l'unique méthode CalculerImpot de l'interface [IImpotMetier] avec vérification des résultats.

Le projet [metier] est maintenant configuré comme suit :

Image non disponible
  • [1] : le projet est de type "bibliothèque de classes"
  • [2] : la génération du projet produira la DLL [ImpotsV5-metier.dll]

Le projet est généré (F6). Puis la DLL [] générée est chargée dans NUnit et testée :

Image non disponible

Ci-dessus, les tests ont été réussis. Nous considérons désormais la couche [metier] opérationnelle. Sa DLL contient toutes les classes du projet dont les classes de test. Celles-ci sont inutiles. Nous reconstruisons la DLL afin d'en exclure les classes de tests.

Image non disponible
  • en [1] : le dossier [tests] est exclu du projet
  • en [2] : le nouveau projet. Celui-ci est régénéré par F6 afin de générer une nouvelle DLL.

VII-D-5. La couche [ui]

Image non disponible
Image non disponible
  • en [1], le projet [ui] est devenu le projet actif de la solution
  • en [2] : les références du projet
  • en [3] : la couche [ui]
  • en [4] : le fichier [DataImpot.txt] des tranches d'impôt, configuré [5] pour être recopié automatiquement dans le dossier d'exécution du projet [6]

Les références du projet (cf [2] dans le projet)

La couche [ui] a besoin des couches [metier] et [dao] pour mener à bien ses calculs d'impôt. Il lui faut donc une référence sur les DLL de ces deux couches. On procède comme il a été montré pour la couche [metier]

La classe principale [Dialogue.cs] (cf [3] dans le projet)

La classe [Dialogue.cs] est celle de la version précédente.

Tests

Le projet [ui] est configuré comme suit :

Image non disponible
  • [1] : le projet est de type "application console"
  • [2] : la génération du projet produira l'exécutable [ImpotsV5-ui.exe]
  • [3] : la classe qui sera exécutée

Un exemple d'exécution (Ctrl+F5) est le suivant :

Cacher Sélectionnez
  1. Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 60000
  2. Impot=4282 euros

VII-D-6. La couche [Spring]

Revenons au code dans [Dialogue.cs] qui crée les couches [dao] et [metier] :

Cacher Sélectionnez
  1. // on crée les couches [metier et dao]
  2.             IImpotMetier metier = null;
  3.             try {
  4.         // création couche [metier]
  5.                 metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
  6.             } catch (ImpotException e) {
  7.                 // affichage erreur
  8. ...
  9.                 // arrêt programme
  10.                 Environment.Exit(1);
  11.             }

La ligne 5 crée les couches [dao] et [metier] en nommant explicitement les classes d'implémentation des deux couches : FileImpot pour la couche [dao], ImpotMetier pour la couche [metier]. Si l'implémentation d'une des couches est faite avec une nouvelle classe, la ligne 5 sera changée. Par exemple :

Cacher Sélectionnez
  1.                 metier = new ImpotMetier(new HardwiredImpot());

En-dehors de ce changement, rien ne changera dans l'application du fait que chaque couche communique avec la suivante selon une interface. Tant que cette dernière ne change pas, la communication entre couches ne change pas non plus. Le framework Spring nous permet d'aller un peu plus loin dans l'indépendance des couches en nous permettant d'externaliser dans un fichier de configuration le nom des classes implémentant les différentes couches. Changer l'implémentation d'une couche revient alors à changer un fichier de configuration. Il n'y a aucun impact sur le code de l'application.

Image non disponible

Ci-dessus, la couche [ui] va demander [0] à Spring d'instancier les couches [dao] [1] et [metier] [2] d'après les informations contenues dans un fichier de configuration. La couche [ui] demandera ensuite à Spring [3], une référence sur la couche [metier] :

Cacher Sélectionnez
  1.             // on crée les couches [metier et dao]
  2.             IImpotMetier metier = null;
  3.             try {
  4.                 // contexte Spring
  5.                 IApplicationContext ctx = ContextRegistry.GetContext();
  6.                 // on demande une référence sur la couche [metier]
  7.                 metier = (IImpotMetier)ctx.GetObject("metier");
  8.             } catch (Exception e1) {
  9. ...
  10. }
  • ligne 5 : instanciation des couches [dao] et [metier] par Spring
  • ligne 7 : on récupère une référence sur la couche [metier]. On notera que la couche [ui] a eu cette référence sans donner le nom de la classe implémentant la couche [metier].

Le framework Spring existe en deux versions : Java et .NET. La version .NET est disponible à l'url (mars 2008) [http://www.springframework.net/] :

Image non disponible
  • en [1] : le site de [Spring.net]
  • en [2] : la page des téléchargements
Image non disponible
  • en [3] : télécharger Spring 1.1 (mars 2008)
Image non disponible
  • en [4] : télécharger la version .exe puis l'installer
  • en [5] : le dossier généré par l'installation
  • en [6] : le dossier [bin/net/2.0/release] contient les DLL de Spring pour les projets Visual Studio .NET 2.0 ou supérieur. Spring est un framework riche. L'aspect de Spring que nous allons utiliser ici pour gérer l'intégration des couches dans une application s'appelle IoC : Inversion of Contro l ou encore DI : Dependence Injection . Spring apporte des bibliothèques pour l'accès aux bases de données avec NHibernate , la génération et l'exploitation de services web, d'applications web…
  • les DLL nécessaires pour gérer l'intégration des couches dans une application sont les DLL [7] et [8].

Nous stockons ces trois DLL dans un dossier [lib] de notre projet :

Image non disponible
  • [1] : les trois DLL sont placées dans le dossier [lib] avec l'explorateur windows
  • [2] : dans le projet [ui], on fait afficher tous les fichiers
  • [3] : le dossier [ui/lib] est désormais visible. On l'inclut dans le projet
  • [4] : le dossier [ui/lib] fait partie du projet

L'opération de création du dossier [lib] n'est nullement indispensable. Les références pouvaient être créées directement sur les trois DLL du dossier [bin/net/2.0/release] de [Spring.net]. La création du dossier [lib] permet cependant de développer l'application sur un poste ne disposant pas de [Spring.net] la rendant ainsi moins dépendante de l'environnement de développement disponible.

Nous ajoutons au projet [ui] des références sur les trois nouvelles DLL :

Image non disponible
  • [1] : on crée des références sur les trois DLL du dossier [lib] [2]
  • [3] : les trois DLL font partie des références du projet

Revenons à une vue d'ensemble de l'architecture de l'application :

Image non disponible

Ci-dessus, la couche [ui] va demander [0] à Spring d'instancier les couches [dao] [1] et [metier] [2] d'après les informations contenues dans un fichier de configuration. La couche [ui] demandera ensuite à Spring [3], une référence sur la couche [metier]. Cela se traduira dans la couche [ui] par le code suivant :

Cacher Sélectionnez
  1.             // on crée les couches [metier et dao]
  2.             IImpotMetier metier = null;
  3.             try {
  4.                 // contexte Spring
  5.                 IApplicationContext ctx = ContextRegistry.GetContext();
  6.                 // on demande une référence sur la couche [metier]
  7.                 metier = (IImpotMetier)ctx.GetObject("metier");
  8.             } catch (Exception e1) {
  9. ...
  10. }
  • ligne 5 : instanciation des couches [dao] et [metier] par Spring
  • ligne 7 : on récupère une référence sur la couche [metier].

La ligne [5] ci-dessus exploite le fichier de configuration [App.config] du projet Visual Studio. Dans un projet C#, ce fichier sert à configurer l'application. [App.config] n'est donc pas une notion Spring mais une notion Visual Studio que Spring exploite. Spring sait exploiter d'autres fichiers de configuration que [App.config]. La solution présentée ici n'est donc pas la seule disponible.

Créons le fichier [App.config] avec l'assistant Visual Studio :

Image non disponible
  • en [1] : ajout d'un nouvel élément au projet
  • en [2] : sélectionner "Application Configuration File"
  • en [3] : [App.config] est le nom par défaut de ce fichier de configuration
  • en [4] : le fichier [App.config] a été ajouté au projet

Le contenu du fichier [App.config] est le suivant :

Cacher Sélectionnez
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3. </configuration>

[] est un fichier XML. La configuration du projet se fait entre les balises <configuration>. La configuration nécessaire à Spring est la suivante :

Cacher Sélectionnez
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.
  4.     <configSections>
  5.         <sectionGroup name="spring">
  6.             <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
  7.             <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
  8.         </sectionGroup>
  9.     </configSections>
  10.
  11.     <spring>
  12.         <context>
  13.             <resource uri="config://spring/objects" />
  14.         </context>
  15.         <objects xmlns="http://www.springframework.net">
  16.             <object name="dao" type="Dao.FileImpot, ImpotsV5-dao">
  17.                 <constructor-arg index="0" value="DataImpot.txt"/>
  18.             </object>
  19.             <object name="metier" type="Metier.ImpotMetier, ImpotsV5-metier">
  20.                 <constructor-arg index="0" ref="dao"/>
  21.             </object>
  22.         </objects>
  23.     </spring>
  24. </configuration>
  • lignes 11-23 : la section délimitée par la balise <spring> est appelée le groupe de sections <spring>. On peut créer autant de groupes de sections que l'on veut dans [App.config].
  • un groupe de sections a des sections : c'est le cas ici :
  • lignes 12-14 : la section <spring/context>
  • lignes 15-22 : la section <spring/objects>
  • lignes 4-9 : la région <configSections> définit la liste des gestionnaires (handlers) des groupes de sections présents dans [App.config].
  • lignes 5-8 : définit la liste des gestionnaires des sections du groupe <spring> (name="spring").
  • ligne 6 : le gestionnaire de la section <context> du groupe <spring> :
  • name : nom de la section gérée
  • type : nom de la classe gérant la section sous la forme NomClasse, NomDLL .
  • la section <context> du groupe <spring> est gérée par la classe [Spring.Context.Support.ContextHandler] qui sera trouvée dans la DLL [Spring.Core.dll]
  • ligne 7 : le gestionnaire de la section <objects> du groupe <spring>

Les lignes 4-9 sont standard dans un fichier [App.config] avec Spring. On se contente de les recopier d'un projet à l'autre.

  • lignes 12-14 : définit la section <spring/context>.
  • ligne 13 : la balise <resource> a pour but d'indiquer où se trouve le fichier définissant les classes que Spring doit instancier. Celles-ci peuvent être dans [App.config] comme ici mais elles peuvent être également dans un fichier de configuration autre. La localisation de ces classes est faite dans l'attribut uri de la balise <resource> :
  • <resource uri="config://spring/objects> indique que la liste des classes à instancier se trouve dans le fichier [App.config] (config:), dans la section //spring/objects, c.a.d. dans la balise <objects> de la balise <spring>.
  • <resource uri="file://spring-config.xml"> indiquerait que la liste des classes à instancier se trouve dans le fichier [spring-config.xml]. Celui-ci devrait être placé dans les dossiers d'exécution (bin/Release ou bin/Debug) du projet. Le plus simple est de le placer, comme il a été fait pour le fichier [DataImpot.txt], à la racine du projet avec la propriété [Copy to output directory=always].

Les lignes 12-14 sont standard dans un fichier [App.config] avec Spring. On se contente de les recopier d'un projet à l'autre.

  • lignes 15-22 : définissent les classes à instancier. C'est dans cette partie que se fait la configuration spécifique d'une application. La balise <objects> délimite la section de définition des classes à instancier.
  • lignes 16-18 : définissent la classe à instancier pour la couche [dao]
  • ligne 16 : chaque objet instancié par Spring fait l'objet d'une balise <object>. Celle-ci a un attribut name qui est le nom de l'objet instancié. C'est via celui-ci que l'application demande à Spring une référence : "donne-moi une référence sur l'objet qui s'appelle dao". L'attribut type définit la classe à instancier sous la forme NomClasse, NomDLL. Ainsi la ligne 16 définit un objet appelé "dao", instance de la classe "Dao.FileImpot" qui se trouve dans la DLL "ImpotsV5-dao.dll". On notera qu'on donne le nom complet de la classe (espace de noms inclus) et que le suffixe .dll n'est pas précisé dans le nom de la DLL.

Une classe peut être instanciée de deux façons avec Spring :

  • via un constructeur particulier auquel on passe des paramètres : c'est ce qui est fait dans les lignes 16-18.
  • via le constructeur par défaut sans paramètres. L'objet est alors initialisé via ses propriétés publiques : la balise <object> a alors des sous-balises <property> pour initialiser ces différentes propriétés. Nous n'avons pas d'exemple de ce cas ici.
Cacher Sélectionnez
  1.         public FileImpot(string fileName);

Les paramètres du constructeur sont définis à l'aide de balises <constructor-arg>.

  • ligne 17 : définit le 1er et seul paramètre du constructeur. L'attribut index est le n° du paramètre du constructeur, l'attribut value sa valeur : <constructor-arg index="i" value="valuei"/>
Cacher Sélectionnez
  1.         public ImpotMetier(IImpotDao dao);
  • ligne 20 : définit le 1er et seul paramètre du constructeur. Ci-dessus, le paramètre dao du constructeur est une référence d'objet. Dans ce cas, dans la balise <constructor-arg> on utilise l'attribut ref au lieu de l'attribut value qui a été utilisé pour la couche [dao] : <constructor-arg index="i" ref="refi"/>. Dans le constructeur ci-dessus, le paramètre dao représente une instance sur la couche [dao]. Cette instance a été définie par les lignes 16-18 du fichier de configuration. Ainsi dans la ligne 20 :
Cacher Sélectionnez
  1.                 <constructor-arg index="0" ref="dao"/>

ref="dao" représente l'objet Spring "dao" défini par les lignes 16-18.

Pour résumer, le fichier [App.config] :

  • instancie la couche [dao] avec la classe FileImpot qui reçoit pour paramètre DataImpot.txt (ligne 16-18). L'objet résultant est appelé "dao"
  • instancie la couche [metier] avec la classe ImpotMetier qui reçoit pour paramètre l'objet "dao" précédent (lignes 19-21).

Il ne nous reste plus qu'à utiliser ce fichier de configuration Spring dans la couche [ui]. Pour cela, nous dupliquons la classe [Dialogue.cs] en [Dialogue2.cs] et nous faisons de cette dernière la classe principale du projet [ui] :

Image non disponible
  • en [1] : copie de [Dialogue.cs]
  • en [2] : collage
  • en [3] : la copie de [Dialogue.cs]
  • en [4] : renommée [Dialogue2.cs]
Image non disponible
  • en [6] : on fait de [Dialogue2.cs] la classe principale du projet [ui].

Le code suivant de [Dialogue.cs] :

Cacher Sélectionnez
  1.             // on crée les couches [metier et dao]
  2.             IImpotMetier metier = null;
  3.             try {
  4.         // création couche [metier]
  5.                 metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
  6.             } catch (ImpotException e) {
  7.                 // affichage erreur
  8.                 string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
  9.                 Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
  10.                 // arrêt programme
  11.                 Environment.Exit(1);
  12.             }
  13.             // boucle infinie
  14.             while (true) {
  15. ...

devient le suivant dans [Dialogue2.cs] :

Cacher Sélectionnez
  1.             // on crée les couches [metier et dao]
  2.             IApplicationContext ctx = null;
  3.             try {
  4.                 // contexte Spring
  5.                 ctx = ContextRegistry.GetContext();
  6.             } catch (Exception e1) {
  7.                 // affichage erreur
  8.                 Console.WriteLine("Chaîne des exceptions : \n{0}", "".PadLeft(40, '-'));
  9.                 Exception e = e1;
  10.                 while (e != null) {
  11.                     Console.WriteLine("{0}: {1}", e.GetType().FullName, e.Message);
  12.                     Console.WriteLine("".PadLeft(40, '-'));
  13.                     e = e.InnerException;
  14.                 }
  15.                 // arrêt programme
  16.                 Environment.Exit(1);
  17.             }
  18.             // on demande une référence sur la couche [metier]
  19.             IImpotMetier metier = (IImpotMetier)ctx.GetObject("metier");
  20.             // boucle infinie
  21.             while (true) {
  22. ....................................
  • ligne 2 : IApplicationContext donne accès à l'ensemble des objets instanciés par Spring. On appelle cet objet, le contexte Spring de l'application ou plus simplement le contexte de l'application. Pour l'instant, ce contexte n'a pas été initialisé. C'est le try / catch qui suit qui le fait.
  • ligne 5 : la configuration de Spring dans [App.config] est lue et exploitée. Après cette opération, s'il n'y a pas eu d'exception, tous les objets de la section <objects> ont été instanciés :
  • l'objet Spring "dao" est une instance sur la couche [dao]
  • l'objet Spring "metier" est une instance sur la couche [metier]
  • ligne 19 : la classe [Dialogue2.cs] a besoin d'une référence sur la couche [metier]. Celle-ci est demandée au contexte de l'application. L'objet IApplicationContext donne accès aux objets Spring via leur nom (attribut name de la balise <object> de la configuration Spring). La référence rendue est une référence sur le type générique Object . On est amenés à transtyper la référence rendue dans le bon type, ici le type de l'interface de la couche [metier] : IImpotMetier .

Si tout s'est bien passé, après la ligne 19, [Dialogue2.cs] a une référence sur la couche [metier]. Le code des lignes 21 et au-delà est celui de la classe [Dialogue.cs] déjà étudiée.

  • lignes 6-17 : gestion de l'exception qui survient lorsque l'exploitation du fichier de configuration de Spring ne peut être menée à son terme. Il peut y avoir diverses raisons à cela : syntaxe incorrecte du fichier de configuration lui-même ou bien impossibilité à instancier l'un des objets configurés. Dans notre exemple, ce dernier cas se produirait si le fichier DataImpot.txt de la ligne 17 de [App.config] n'était pas trouvé dans le dossier d'exécution du projet.

L'exception qui remonte ligne 6 est une chaîne d'exceptions où chaque exception a deux propriétés :

  • Message : le message d'erreur liée à l'exception
  • InnerException : l'exception précédente dans la chaîne des exceptions

La boucle des lignes 10-14 fait afficher toutes les exceptions de la chaîne sous la forme : classe de l'exception et message associé.

Lorsqu'on exécute le projet [ui] avec un fichier de configuration valide, on obtient les résultats habituels :

Cacher Sélectionnez
  1. Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 60000
  2. Impot=4282 euros

Lorsqu'on exécute le projet [ui] avec un fichier [DataImpotInexistant.txt] inexistant,

Cacher Sélectionnez
  1.             <object name="dao" type="Dao.FileImpot, ImpotsV5-dao">
  2.                 <constructor-arg index="0" value="DataImpotInexistant.txt"/>
  3.             </object>

on obtient les résultats suivants :

Cacher Sélectionnez
  1. Chaîne des exceptions :
  2. ----------------------------------------
  3. System.Configuration.ConfigurationErrorsException: Error creating context 'spring.root': Could not find file 'C:\data\2007-2008\c# 2008\poly\Chap4\ImpotsV5\ui\bin\Release\DataImpotInexistant.txt'.
  4. ----------------------------------------
  5. Spring.Util.FatalReflectionException: Cannot instantiate Type [Spring.Context.Support.XmlApplicationContext] using ctor [Void .ctor(System.String, Boolean, System.String[])] : 'Exception has been thrown by the target of an invocation.'
  6. ----------------------------------------
  7. System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
  8. ----------------------------------------
  9. Spring.Objects.Factory.ObjectCreationException: Error creating object with name'dao' defined in 'config [spring/objects]' : Initialization of object failed : Cannot instantiate Type [Dao.FileImpot] using ctor [Void .ctor(System.String)] :'Exception has been thrown by the target of an invocation.'
  10. ----------------------------------------
  11. Spring.Util.FatalReflectionException: Cannot instantiate Type [Dao.FileImpot] using ctor [Void .ctor(System.String)] : 'Exception has been thrown by the targetof an invocation.'
  12. ----------------------------------------
  13. System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
  14. ----------------------------------------
  15. Entites.ImpotException: Erreur lors de la lecture du fichier DataImpotInexistant.txt
  16. ----------------------------------------
  17. System.IO.FileNotFoundException: Could not find file 'C:\data\2007-2008\c# 2008\poly\Chap4\ImpotsV5\ui\bin\Release\DataImpotInexistant.txt'.
  • ligne 17 : l'exception originelle de type [FileNotFoundException]
  • ligne 15 : la couche [dao] encapsule cette exception dans un type [Entites.ImpotException]
  • ligne 9 : l'exception lancée par Spring parce qu'il n'a pas réussi à instancier l'objet nommé "dao". Dans le processus de création de cet objet, deux autres exceptions sont intervenues auparavant : celles des lignes 11 et 13.
  • parce que l'objet "dao" n'a pu être créé, le contexte de l'application n'a pu être créé. C'est le sens de l'exception ligne 5. Auparavant, une autre exception, celle de la ligne 7 s'était produite.
  • ligne 3 : l'exception de plus haut niveau, la dernière de la chaîne : une erreur de configuration est signalée.

De tout cela, on retiendra que c'est l'exception la plus profonde, ici celle de la ligne 17 qui est souvent la plus significative. On notera cependant que Spring a conservé le message d'erreur de la ligne 17 pour le remonter à l'exception de plus haut niveau ligne 3 afin d'avoir la cause originelle de l'erreur au niveau le plus haut.

Spring mérite à lui tout seul un livre. Nous n'avons fait ici qu'effleurer le sujet. On pourra l'approfondir avec le document [spring-net-reference.pdf] qu'on trouve dans le dossier d'installation de Spring :

Image non disponible

On pourra lire également [http://tahe.developpez.com/dotnet/springioc], un tutoriel Spring présenté dans un contexte VB.NET.


CSharp tutoriel 2008

Apprentissage du langage C#

  • VIII. Interfaces graphiques avec C# et VS.NET
    • VIII-A. Les bases des interfaces graphiques
      • VIII-A-1. Un premier projet
      • VIII-A-2. Un second projet
        • VIII-A-2-a. Le formulaire
        • VIII-A-2-b. Le code lié à la gestion des événements
        • VIII-A-2-c. Conclusion
    • VIII-B. Les composants de base
      • VIII-B-1. Formulaire Form
      • VIII-B-2. Etiquettes Label et boîtes de saisie TextBox
      • VIII-B-3. Listes déroulantes ComboBox
      • VIII-B-4. Composant ListBox
      • VIII-B-5. Cases à cocher CheckBox, boutons radio ButtonRadio
      • VIII-B-6. Variateurs ScrollBar
    • VIII-C. Événements souris
    • VIII-D. Créer une fenêtre avec menu
    • VIII-E. Composants non visuels
      • VIII-E-1. Boîtes de dialogue OpenFileDialog et SaveFileDialog
      • VIII-E-2. Boîtes de dialogue FontColor et ColorDialog
      • VIII-E-3. Timer
    • VIII-F. Application exemple - version 6
      • VIII-F-1. La solution Visual Studio
      • VIII-F-2. La classe [Program.cs]
      • VIII-F-3. Le formulaire [Form1]
    • VIII-G. Le formulaire [Form2]
      • VIII-G-1. Conclusion

VIII. Interfaces graphiques avec C# et VS.NET

VIII-A. Les bases des interfaces graphiques

VIII-A-1. Un premier projet

Construisons un premier projet de type "Application windows" :

Image non disponible
  • [1] : créer un nouveau projet
  • [2] : de type Application Windows
  • [3] : le nom du projet importe peu pour le moment
  • [4] : le projet créé
Image non disponible
  • [5] : on sauvegarde la solution courante
  • [6] : nom du projet
  • [7] : dossier de la solution
  • [8] : nom de la solution
  • [9] : un dossier sera créé pour la solution [Chap5]. Les projets de celle-ci seront dans des sous-dossiers.
Image non disponible
  • [10] : le projet [01] dans la solution [Chap5] :
  • [Program.cs] est la classe principale du projet
  • [Form1.cs] est le fichier source qui va gérer le comportement de la fenêtre [11]
  • [Form1.Designer.cs] est le fichier source qui va encapsuler l'information sur les composants de la fenêtre [11]
  • [11] : le fichier [Form1.cs] en mode "conception" (design)
  • [12] : l'application générée peut être exécutée par (Ctrl-F5). La fenêtre [Form1] s'affiche. On peut la déplacer, la redimensionner et la fermer. On a donc les éléments de base d'une fenêtre graphique.

La classe principale [Program.cs] est la suivante :

Cacher Sélectionnez
  1. using System;
  2. using System.Windows.Forms;
  3.
  4. namespace Chap5 {
  5.     static class Program {
  6.         /// <summary>
  7.         /// The main entry point for the application.
  8.         /// </summary>
  9.         [STAThread]
  10.         static void Main() {
  11.             Application.EnableVisualStyles();
  12.             Application.SetCompatibleTextRenderingDefault(false);
  13.             Application.Run(new Form1());
  14.         }
  15.     }
  16. }
  • ligne 2 : les applications avec formulaires utilisent l'espace de noms System.Windows.Forms .
  • ligne 4 : l'espace de noms initial a été renommé en Chap5 .
  • ligne 10 : à l'exécution du projet (Ctrl-F5), la méthode [Main] est exécutée.
  • lignes 11-13 : la classe Application appartient à l'espace de noms System.Windows.Forms . Elle contient des méthodes statiques pour lancer / arrêter les applications graphiques windows.
  • ligne 11 : facultative - permet de donner différents styles visuels aux contrôles déposés sur un formulaire
  • ligne 12 : facultative - fixe le moteur de rendu des textes des contrôles : GDI+ (true), GDI (false)
  • ligne 13 : la seule ligne indispensable de la méthode [Main] : instancie la classe [Form1] qui est la classe du formulaire et lui demande de s'exécuter.

Le fichier source [Form1.cs] est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Windows.Forms;
  3.
  4. namespace Chap5 {
  5.     public partial class Form1 : Form {
  6.         public Form1() {
  7.             InitializeComponent();
  8.         }
  9.     }
  10. }
  • ligne 5 : la classe Form1 dérive de la classe [System.Windows.Forms.Form] qui est la classe mère de toutes les fenêtres. Le mot clé partial indique que la classe est partielle et qu'elle peut être complétée par d'autres fichiers source. C'est le cas ici, où la classe Form1 est répartie dans deux fichiers :
  • [Form1.cs] : dans lequel on trouvera le comportement du formulaire, notamment ses gestionnaires d'événements
  • [Form1.Designer.cs] : dans lequel on trouvera les composants du formulaire et leurs propriétés. Ce fichier a la particularité d'être régénéré à chaque fois que l'utilisateur modifie la fenêtre en mode [conception].
  • lignes 6-8 : le constructeur de la classe Form1
  • ligne 7 : fait appel à la méthode InitializeComponent . On voit que cette méthode n'est pas présente dans [Form1.cs]. On la trouve dans [Form1.Designer.cs].

Le fichier source [Form1.Designer.cs] est le suivant :

Cacher Sélectionnez
  1. namespace Chap5 {
  2.     partial class Form1 {
  3.         /// <summary>
  4.         /// Required designer variable.
  5.         /// </summary>
  6.         private System.ComponentModel.IContainer components = null;
  7.
  8.         /// <summary>
  9.         /// Clean up any resources being used.
  10.         /// </summary>
  11.         /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
  12.         protected override void Dispose(bool disposing) {
  13.             if (disposing && (components != null)) {
  14.                 components.Dispose();
  15.             }
  16.             base.Dispose(disposing);
  17.         }
  18.
  19.         #region Windows Form Designer generated code
  20.
  21.         /// <summary>
  22.         /// Required method for Designer support - do not modify
  23.         /// the contents of this method with the code editor.
  24.         /// </summary>
  25.         private void InitializeComponent() {
  26.             this.SuspendLayout();
  27.             //
  28.             // Form1
  29.             //
  30.             this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
  31.             this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
  32.             this.ClientSize = new System.Drawing.Size(196, 98);
  33.             this.Name = "Form1";
  34.             this.Text = "Form1";
  35.             this.ResumeLayout(false);
  36.
  37.         }
  38.
  39.         #endregion
  40.
  41.     }
  42. }
  • ligne 2 : il s'agit toujours de la classe Form1 . On notera qu'il n'est plus besoin de répéter qu'elle dérive de la classe Form .
  • lignes 25-37 : la méthode InitializeComponent appelée par le constructeur de la classe [Form1]. Cette méthode va créer et initialiser tous les composants du formulaire. Elle est régénérée à chaque changement de celui-ci en mode [conception]. Une section, appelée région , est créée pour la délimiter lignes 19-39. Le développeur ne doit pas ajouter de code dans cette région : il sera écrasé à la régénération suivante.

Il est plus simple dans un premier temps de ne pas s'intéresser au code de [Form1.Designer.cs]. Il est généré automatiquement et est la traduction en langage C# des choix que le développeur fait en mode [conception]. Prenons un premier exemple :

Image non disponible
  • [1] : sélectionner le mode [conception] en double-cliquant sur le fichier [Form1.cs]
  • [2] : cliquer droit sur le formulaire et choisir [Properties]
  • [3] : la fenêtre des propriétés de [Form1]
  • [4] : la propriété [Text] représente le titre de la fenêtre
  • [5] : le changement de la propriété [Text] est pris en compte en mode [conception] ainsi que dans le code source [Form1.Designer.cs] :
Cacher Sélectionnez
  1.         private void InitializeComponent() {
  2.             this.SuspendLayout();
  3. ...
  4.             this.Text = "Mon 1er formulaire";
  5. ...
  6. }

VIII-A-2. Un second projet

VIII-A-2-a. Le formulaire

Nous commençons un nouveau projet appelé 02 . Pour cela nous suivons la procédure explicitée précédemment pour créer un projet. La fenêtre à créer est la suivante :

Image non disponible

Les composants du formulaire sont les suivants :

n° | nom | type | rôle
1 | labelSaisie | Label | un libellé
2 | textBoxSaisie | TextBox | une zone de saisie
3 | buttonAfficher | Button | pour afficher dans une boîte de dialogue le contenu de la zone de saisie textBoxSaisie

On pourra procéder comme suit pour construire cette fenêtre :

Image non disponible
  • [1] : cliquer droit sur le formulaire en-dehors de tout composant et choisir l'option [Properties]
  • [2] : la feuille de propriétés de la fenêtre apparaît dans le coin inférieur droit de Visual studio

Parmi les propriétés du formulaire à noter :

BackColor | pour fixer la couleur de fond de la fenêtre
ForeColor | pour fixer la couleur des dessins ou du texte sur la fenêtre
Menu | pour associer un menu à la fenêtre
Text | pour donner un titre à la fenêtre
FormBorderStyle | pour fixer le type de fenêtre
Font | pour fixer la police de caractères des écritures dans la fenêtre
Name | pour fixer le nom de la fenêtre

Ici, nous fixons les propriétés Text et Name :

Text | Saisies et boutons - 1
Name | frmSaisiesBoutons
Image non disponible
  • [1] : choisir la boîte à outils [Common Controls] parmi les boîtes à outils proposées par Visual Studio
  • [2, 3, 4] : double-cliquer successivement sur les composants [Label], [Button] et [TextBox]
  • [5] : les trois composants sont sur le formulaire

Pour aligner et dimensionner correctement les composants, on peut utiliser les éléments de la barre d'outils :

Image non disponible
Image non disponible

Le principe du formatage est le suivant :

  • sélectionnez les différents composants à formater ensemble (touche Ctrl appuyée pendant les différents clics sélectionnant les composants)
  • sélectionnez le type de formatage désiré :

Une fois placés les composants nous fixons leurs propriétés. Pour cela, cliquer droit sur le composant et prendre l'option Properties :

Image non disponible
  • [1] : sélectionner le composant pour avoir sa fenêtre de propriétés. Dans celle-ci, modifier les propriétés suivantes : name : labelSaisie , text : Saisie
  • [2] : procéder de même : name : textBoxSaisie , text : ne rien mettre
  • [3] : name : buttonAfficher , text : Afficher
  • [4] : la fenêtre elle-même : name : frmSaisiesBoutons , text : Saisies et boutons - 1
  • [5] : exécuter (Ctrl-F5) le projet pour avoir un premier aperçu de la fenêtre en action.

Ce qui a été fait en mode [conception] a été traduit dans le code de [Form1.Designer.cs] :

Cacher Sélectionnez
  1. namespace Chap5 {
  2.     partial class frmSaisiesBoutons {
  3. ...
  4.         private System.ComponentModel.IContainer components = null;
  5. ...
  6.         private void InitializeComponent() {
  7.             this.labelSaisie = new System.Windows.Forms.Label();
  8.             this.buttonAfficher = new System.Windows.Forms.Button();
  9.             this.textBoxSaisie = new System.Windows.Forms.TextBox();
  10.             this.SuspendLayout();
  11.             //
  12.             // labelSaisie
  13.             //
  14.             this.labelSaisie.AutoSize = true;
  15.             this.labelSaisie.Location = new System.Drawing.Point(12, 19);
  16.             this.labelSaisie.Name = "labelSaisie";
  17.             this.labelSaisie.Size = new System.Drawing.Size(35, 13);
  18.             this.labelSaisie.TabIndex = 0;
  19.             this.labelSaisie.Text = "Saisie";
  20.             //
  21.             // buttonAfficher
  22.             //
  23.             this.buttonAfficher.Location = new System.Drawing.Point(80, 49);
  24.             this.buttonAfficher.Name = "buttonAfficher";
  25.             this.buttonAfficher.Size = new System.Drawing.Size(75, 23);
  26.             this.buttonAfficher.TabIndex = 1;
  27.             this.buttonAfficher.Text = "Afficher";
  28.             this.buttonAfficher.UseVisualStyleBackColor = true;
  29.             this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);
  30.             //
  31.             // textBoxSaisie
  32.             //
  33.             this.textBoxSaisie.Location = new System.Drawing.Point(80, 19);
  34.             this.textBoxSaisie.Name = "textBoxSaisie";
  35.             this.textBoxSaisie.Size = new System.Drawing.Size(100, 20);
  36.             this.textBoxSaisie.TabIndex = 2;
  37.             //
  38.             // frmSaisiesBoutons
  39.             //
  40.             this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
  41.             this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
  42.             this.ClientSize = new System.Drawing.Size(292, 118);
  43.             this.Controls.Add(this.textBoxSaisie);
  44.             this.Controls.Add(this.buttonAfficher);
  45.             this.Controls.Add(this.labelSaisie);
  46.             this.Name = "frmSaisiesBoutons";
  47.             this.Text = "Saisies et boutons - 1";
  48.             this.ResumeLayout(false);
  49.             this.PerformLayout();
  50.
  51.         }
  52.
  53.         private System.Windows.Forms.Label labelSaisie;
  54.         private System.Windows.Forms.Button buttonAfficher;
  55.         private System.Windows.Forms.TextBox textBoxSaisie;
  56.
  57.     }
  58. }
  • lignes 53-55 : les trois composants ont donné naissance à trois champs privés de la classe [Form1]. On notera que les noms de ces champs sont les noms donnés aux composants en mode [conception]. C'est le cas également du formulaire ligne 2 qui est la classe elle-même.
  • lignes 7-9 : les trois objets de type [Label], [TextBox] et [Button] sont créés. C'est à travers eux que les composants visuels sont gérés.
  • lignes 14-19 : configuration du label labelSaisie
  • lignes 23-29 : configuration du bouton buttonAfficher
  • lignes 33-36 : configuration du champ de saisie textBoxSaisie
  • lignes 40-47 : configuration du formulaire frmSaisiesBoutons . On notera, lignes 43-45, la façon d'ajouter des composants au formulaire.

Ce code est compréhensible. Il est ainsi possible de construire des formulaires par code sans utiliser le mode [conception]. De nombreux exemples de ceci sont donnés dans la documentation MSDN de Visual Studio. Maîtriser ce code permet de créer des formulaires en cours d'exécution : par exemple, créer à la volée un formulaire permettant la mise à jour d'une table de base de données, la structure de cette table n'étant découverte qu'à l'exécution.

Il nous reste à écrire la procédure de gestion d'un clic sur le bouton Afficher . Sélectionner le bouton pour avoir accès à sa fenêtre de propriétés. Celle-ci a plusieurs onglets :

Image non disponible
  • [1] : liste des propriétés par ordre alphabétique
  • [2] : événements liés au contrôle

Les propriétés et événements d'un contrôle sont accessibles par catégories ou par ordre alphabétique :

  • [3] : Propriétés ou événements par catégorie
  • [4] : Propriétés ou événements par ordre alphabétique

L'onglet Events en mode Catégories pour le bouton buttonAfficher est le suivant :

Image non disponible
  • [1] : la colonne de gauche de la fenêtre liste les événements possibles sur le bouton. Un clic sur un bouton correspond à l'événement Click .
  • [2] : la colonne de droite contient le nom de la procédure appelée lorsque l'événement correspondant se produit.
  • [3] : si on double-clique sur la cellule de l'événement Click , on passe alors automatiquement dans la fenêtre de code pour écrire le gestionnaire de l'événement Click sur le bouton buttonAfficher :
Cacher Sélectionnez
  1. using System;
  2. using System.Windows.Forms;
  3.
  4. namespace Chap5 {
  5.     public partial class frmSaisiesBoutons : Form {
  6.         public frmSaisiesBoutons() {
  7.             InitializeComponent();
  8.         }
  9.
  10.         private void buttonAfficher_Click(object sender, EventArgs e) {
  11.
  12.         }
  13.     }
  14. }
  • lignes 10-12 : le squelette du gestionnaire de l'événement Click sur le bouton nommé buttonAfficher . On notera les points suivants :
  • la méthode est nommée selon le schéma nomDuComposant_NomEvénement
  • la méthode est privée. Elle reçoit deux paramètres :
  • sender : est l'objet qui a provoqué l'événement. Si la procédure est exécutée à la suite d'un clic sur le bouton buttonAfficher , sender sera égal à buttonAfficher . On peut imaginer que la procédure buttonAfficher_Click soit exécutée à partir d'une autre procédure. Celle-ci aurait alors tout loisir de mettre comme premier paramètre, l'objet sender de son choix.
  • EventArgs : un objet qui contient des informations sur l'événement. Pour un événement Click , il ne contient rien. Pour un événement ayant trait aux déplacements de la souris, on y trouvera les coordonnées (X,Y) de la souris.
  • nous n'utiliserons aucun de ces paramètres ici.

Ecrire un gestionnaire d'événement consiste à compléter le squelette de code précédent. Ici, nous voulons présenter une boîte de dialogue avec dedans, le contenu du champ textBoxSaisie s'il est non vide [1], un message d'erreur sinon [2] :

Image non disponible

Le code réalisant cela pourrait-être le suivant :

Cacher Sélectionnez
  1.         private void buttonAfficher_Click(object sender, EventArgs e) {
  2.             // on affiche le texte qui a été saisi dans le TextBox textboxSaisie
  3.             string texte = textBoxSaisie.Text.Trim();
  4.             if (texte.Length != 0) {
  5.                 MessageBox.Show("Texte saisi= " + texte, "Vérification de la saisie", MessageBoxButtons.OK, MessageBoxIcon.Information);
  6.             } else {
  7.                 MessageBox.Show("Saissez un texte...", "Vérification de la saisie", MessageBoxButtons.OK, MessageBoxIcon.Error);
  8. }

La classe MessageBox sert à afficher des messages dans une fenêtre. Nous avons utilisé ici la méthode Show suivante :

Cacher Sélectionnez
  1. public static DialogResult Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon);

avec

text | le message à afficher
caption | le titre de la fenêtre
buttons | les boutons présents dans la fenêtre
icon | l'icone présente dans la fenêtre

Le paramètre buttons peut prendre ses valeurs parmi les constantes suivantes (préfixées par MessageBoxButtons comme montré ligne 7) ci-dessus :

constante | boutons
AbortRetryIgnore | Image non disponible
OK | Image non disponible
OKCancel | Image non disponible
RetryCancel | Image non disponible
YesNo | Image non disponible
YesNoCancel | Image non disponible

Le paramètre icon peut prendre ses valeurs parmi les constantes suivantes (préfixées par MessageBoxIcon comme montré ligne 10) ci-dessus :

Asterisk | Image non disponible | Error | idem Stop
Exclamation | idem Warning | Hand | Image non disponible
Information | idem Asterisk | None | Image non disponible
Question | Image non disponible | Stop | idem Hand
Warning | Image non disponible |  |

La méthode Show est une méthode statique qui rend un résultat de type [System.Windows.Forms.DialogResult] qui est une énumération :

Image non disponible

Pour savoir sur quel bouton a appuyé l'utilisateur pour fermer la fenêtre de type MessageBox on écrira :

Cacher Sélectionnez
  1. DialogResult res=MessageBox.Show(..);
  2. if (res==DialogResult.Yes){ // il a appuyé sur le bouton oui...}

VIII-A-2-b. Le code lié à la gestion des événements

Outre la fonction buttonAfficher_Click que nous avons écrite, Visual studio a généré dans la méthode InitializeComponents de [Form1.Designer.cs] qui crée et initialise les composants du formulaire, la ligne suivante :

Cacher Sélectionnez
  1.             this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);

Click est un événement de la classe Button [1, 2, 3] :

Image non disponible
  • [5] : la déclaration de l'événement [Control.Click] [4]. Ainsi on voit que l'événement Click n'est pas propre à la classe [Button]. Il appartient à la classe [Control], classe parente de la classe [Button].
  • EventHandler est un prototype (un modèle ) de méthode appelé delegate. Nous y allons y revenir.
  • event est un mot clé qui restreint les fonctionnalités du delegate EventHandler : un objet delegate a des fonctionnalités plus riches qu'un objet event .

Le delegate EventHandler est défini comme suit :

Image non disponible

Le delegate EventHandler désigne un modèle de méthode :

  • ayant pour 1er paramètre un type Object
  • ayant pour 2ième paramètre un type EventArgs
  • ne rendant aucun résultat

C'est le cas de la méthode de gestion du clic sur le bouton buttonAfficher qui a été générée par Visual Studio :

Cacher Sélectionnez
  1.         private void buttonAfficher_Click(object sender, EventArgs e);

Ainsi la méthode buttonAfficher_Click correspond au prototype défini par le type EventHandler . Pour créer un objet de type EventHandler , on procède comme suit :

Cacher Sélectionnez
  1. EventHandler evtHandler=new EventHandler(méthode correspondant au prototype  défini par le type EventHandler);

Puisque la méthode buttonAfficher_Click correspond au prototype défini par le type EventHandler , on pourra écrire :

Cacher Sélectionnez
  1. EventHandler evtHandler=new EventHandler(buttonAfficher_Click);

Une variable de type delegate est en fait une liste de références sur des méthodes du type du delegate . Pour ajouter une nouvelle méthode M à la variable evtHandler ci-dessus, on utilisera la syntaxe :

Cacher Sélectionnez
  1. evtHandler+=new EvtHandler(M);

La notation += peut être utilisée même si evtHandler est une liste vide.

Revenons à la ligne de [InitializeComponent] qui ajoute un gestionnaire d'événement à l'événement Click de l'objet buttonAfficher :

Cacher Sélectionnez
  1.             this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);

Cette instruction ajoute une méthode de type EventHandler à la liste des méthodes du champ buttonAfficher.Click . Ces méthodes seront appelées à chaque fois que l'événement Click sur le composant buttonAfficher sera détecté. Il n'y en a souvent qu'une. On l'appelle le "gestionnaire de l'événement".

Revenons sur la signature de EventHandler :

Cacher Sélectionnez
  1.         private delegate void EventHandler(object sender, EventArgs e);

Le second paramètre du delegate est un objet de type EventArgs ou d'une classe dérivée. Le type EventArgs est très général et n'apporte en fait aucune information sur l'événement qui s'est produit. Pour un clic sur un bouton, c'est suffisant. Pour un déplacement de souris sur un formulaire, on aurait un événement MouseMove de la classe [Form] défini par :

Cacher Sélectionnez
  1. public event MouseEventHandler MouseMove;

Le delegate MouseEventHandler est défini comme :

Image non disponible

C'est une fonction déléguée (delegate) de signature void f (object, MouseEventArgs) . La classe MouseEventArgs est elle définie par :

Image non disponible

La classe MouseEventArgs est plus riche que la classe EventArgs . On peut par exemple connaître les coordonnées de la souris X et Y au moment où se produit l'événement.

VIII-A-2-c. Conclusion

Des deux projets étudiés, nous pouvons conclure qu'une fois l'interface graphique construite avec Visual studio, le travail du développeur consiste principalement à écrire les gestionnaires des événements qu'il veut gérer pour cette interface graphique. Du code est généré automatiquement par Visual Studio. Ce code, qui peut être complexe, peut être ignoré en première approche. Ultérieurement, son étude peut permettre une meilleure compréhension de la création et de la gestion des formulaires.

VIII-B. Les composants de base

Nous présentons maintenant diverses applications mettant en jeu les composants les plus courants afin de découvrir les principales méthodes et propriétés de ceux-ci. Pour chaque application, nous présentons l'interface graphique et le code intéressant, principalement celui des gestionnaires d'événements.

VIII-B-1. Formulaire Form

Nous commençons par présenter le composant indispensable, le formulaire sur lequel on dépose des composants. Nous avons déjà présenté quelques-unes de ses propriétés de base. Nous nous attardons ici sur quelques événements importants d'un formulaire.

Load | le formulaire est en cours de chargement
Closing | le formulaire est en cours de fermeture
Closed | le formulaire est fermé

L'événement Load se produit avant même que le formulaire ne soit affiché. L'événement Closing se produit lorsque le formulaire est en cours de fermeture. On peut encore arrêter cette fermeture par programmation.

Nous construisons un formulaire de nom Form1 sans composant :

Image non disponible
  • [1] : le formulaire
  • [2] : les trois événements traités

Le code de [Form1.cs] est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Windows.Forms;
  3.
  4. namespace Chap5 {
  5.     public partial class Form1 : Form {
  6.         public Form1() {
  7.             InitializeComponent();
  8.         }
  9.
  10.         private void Form1_Load(object sender, EventArgs e) {
  11.             // chargement initial du formulaire
  12.             MessageBox.Show("Evt Load", "Load");
  13.         }
  14.
  15.         private void Form1_FormClosing(object sender, FormClosingEventArgs e) {
  16.             // le formulaire est en train de se fermer
  17.             MessageBox.Show("Evt FormClosing", "FormClosing");
  18.             // on demande confirmation
  19.             DialogResult réponse = MessageBox.Show("Voulez-vous vraiment quitter l'application", "Closing", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
  20.             if (réponse == DialogResult.No)
  21.                 e.Cancel = true;
  22.         }
  23.
  24.         private void Form1_FormClosed(object sender, FormClosedEventArgs e) {
  25.             // le formulaire va être fermé
  26.             MessageBox.Show("Evt FormClosed", "FormClosed");
  27.         }
  28.     }
  29. }

Nous utilisons la fonction MessageBox pour être averti des différents événements.

ligne 10 : L'événement Load va se produire au démarrage de l'application avant même que le formulaire ne s'affiche : | Image non disponible
ligne 15 : L'événement FormClosing va se produire lorsque l'utilisateur ferme la fenêtre. | Image non disponible
ligne 19 : Nous lui demandons alors s'il veut vraiment quitter l'application : | Image non disponible
ligne 20 : S'il répond Non, nous fixons la propriété Cancel de l'événement CancelEventArgs e que la méthode a reçu en paramètre. Si nous mettons cette propriété à False , la fermeture de la fenêtre est abandonnée, sinon elle se poursuit. L'événement FormClosed va alors se produire : | Image non disponible

VIII-B-2. Etiquettes Label et boîtes de saisie TextBox

Nous avons déjà rencontré ces deux composants. Label est un composant texte et TextBox un composant champ de saisie. Leur propriété principale est Text qui désigne soit le contenu du champ de saisie soit le texte du libellé. Cette propriété est en lecture/écriture.

L'événement habituellement utilisé pour TextBox est TextChanged qui signale que l'utilisateur à modifié le champ de saisie. Voici un exemple qui utilise l'événement TextChanged pour suivre les évolutions d'un champ de saisie :

Image non disponible
n° | type | nom | rôle
1 | TextBox | textBoxSaisie | champ de saisie
2 | Label | labelControle | affiche le texte de 1 en temps réel AutoSize=False, Text=(rien)
3 | Button | buttonEffacer | pour effacer les champs 1 et 2
4 | Button | buttonQuitter | pour quitter l'application

Le code de cette application est le suivant :

Cacher Sélectionnez
  1. using System.Windows.Forms;
  2.
  3. namespace Chap5 {
  4.     public partial class Form1 : Form {
  5.         public Form1() {
  6.             InitializeComponent();
  7.         }
  8.
  9.         private void textBoxSaisie_TextChanged(object sender, System.EventArgs e) {
  10.             // le contenu du TextBox a changé - on le copie dans le Label labelControle
  11.             labelControle.Text = textBoxSaisie.Text;
  12.         }
  13.
  14.         private void buttonEffacer_Click(object sender, System.EventArgs e) {
  15.             // on efface le contenu de la boîte de saisie
  16.             textBoxSaisie.Text = "";
  17.         }
  18.
  19.         private void buttonQuitter_Click(object sender, System.EventArgs e) {
  20.             // clic sur bouton Quitter - on quitte l'application
  21.             Application.Exit();
  22.         }
  23.
  24.         private void Form1_Shown(object sender, System.EventArgs e) {
  25.             // on met le focus sur le champ de saisie
  26.             textBoxSaisie.Focus();
  27.         }
  28.     }
  29. }
  • ligne 24 : l'événement [Form].Shown a lieu lorsque le formulaire est affiché
  • ligne 26 : on met alors le focus (pour une saisie) sur le composant textBoxSaisie .
  • ligne 9 : l'événement [TextBox].TextChanged se produit à chaque fois que le contenu d'un composant TextBox change
  • ligne 11 : on recopie le contenu du composant [TextBox] dans le composant [Label]
  • ligne 14 : gère le clic sur le bouton [Effacer]
  • ligne 16 : on met la chaîne vide dans le composant [TextBox]
  • ligne 19 : gère le clic sur le bouton [Quitter]
  • ligne 21 : pour arrêter l'application en cours d'exécution. On se rappelle que l'objet Application sert à lancer l'application dans la méthode [Main] de [Form1.cs] :
Cacher Sélectionnez
  1.         static void Main() {
  2.             Application.EnableVisualStyles();
  3.             Application.SetCompatibleTextRenderingDefault(false);
  4.             Application.Run(new Form1());
  5. }

L'exemple suivant utilise un TextBox multilignes :

Image non disponible

La liste des contrôles est la suivante :

n° | type | nom | rôle
1 | TextBox | textBoxLignes | champ de saisie multilignes Multiline=true, ScrollBars=Both, AcceptReturn=True, AcceptTab=True
2 | TextBox | textBoxLigne | champ de saisie monoligne
3 | Button | buttonAjouter | Ajoute le contenu de 2 à 1

Pour qu'un TextBox devienne multilignes on positionne les propriétés suivantes du contrôle :

Multiline=true | pour accepter plusieurs lignes de texte
ScrollBars=( None, Horizontal, Vertical, Both) | pour demander à ce que le contrôle ait des barres de défilement (Horizontal, Vertical, Both) ou non (None)
AcceptReturn=(True, False) | si égal à true, la touche Entrée fera passer à la ligne
AcceptTab=(True, False) | si égal à true, la touche Tab générera une tabulation dans le texte

L'application permet de taper des lignes directement dans [1] ou d'en ajouter via [2] et [3].

Le code de l'application est le suivant :

Cacher Sélectionnez
  1. using System.Windows.Forms;
  2. using System;
  3.
  4. namespace Chap5 {
  5.     public partial class Form1 : Form {
  6.         public Form1() {
  7.             InitializeComponent();
  8.         }
  9.
  10.         private void buttonAjouter_Click(object sender, System.EventArgs e) {
  11.             // ajout du contenu de textBoxLigne à celui de textBoxLignes
  12.             textBoxLignes.Text += textBoxLigne.Text+Environment.NewLine;
  13.             textBoxLigne.Text = "";
  14.         }
  15.
  16.         private void Form1_Shown(object sender, EventArgs e) {
  17.             // on met le focus sur le champ de saisie
  18.             textBoxLigne.Focus();
  19.         }
  20.     }
  21. }
  • ligne 18 : lorsque le formulaire est affiché (évt Shown ), on met le focus sur le champ de saisie textBoxLigne
  • ligne 10 : gère le clic sur le bouton [Ajouter]
  • ligne 12 : le texte du champ de saisie textBoxLigne est ajouté au texte du champ de saisie textBoxLignes suivi d'un saut de ligne.
  • ligne 13 : le champ de saisie textBoxLigne est effacé

VIII-B-3. Listes déroulantes ComboBox

Nous créons le formulaire suivant :

Image non disponible
n° | type | nom | rôle
1 | ComboBox | comboNombres | contient des chaînes de caractères DropDownStyle= DropDownList

Un composant ComboBox est une liste déroulante doublée d'une zone de saisie : l'utilisateur peut soit choisir un élément dans (2) soit taper du texte dans (1). Il existe trois sortes de ComboBox fixées par la propriété DropDownStyle :

Simple | liste non déroulante avec zone d'édition
DropDown | liste déroulante avec zone d'édition
DropDownList | liste déroulante sans zone d'édition

Par défaut, le type d'un ComboBox est DropDown .

La classe ComboBox a un seul constructeur :

new ComboBox() crée un combo vide

Les éléments du ComboBox sont disponibles dans la propriété Items :

Cacher Sélectionnez
  1. public ComboBox.ObjectCollection Items {get;}

C'est une propriété indexée, Items[i] désignant l'élément i du Combo. Elle est en lecture seule.

Soit C un combo et C.Items sa liste d'éléments. On a les propriétés suivantes :

C.Items.Count | nombre d'éléments du combo
C.Items[i] | élément i du combo
C.Add(object o) | ajoute l'objet o en dernier élément du combo
C.AddRange(object[] objets) | ajoute un tableau d'objets en fin de combo
C.Insert(int i, object o) | ajoute l'objet o en position i du combo
C.RemoveAt(int i) | enlève l'élément i du combo
C.Remove(object o) | enlève l'objet o du combo
C.Clear() | supprime tous les éléments du combo
C.IndexOf(object o) | rend la position i de l'objet o dans le combo
C.SelectedIndex | index de l'élément sélectionné
C.SelectedItem | élément sélectionné
C.SelectedItem.Text | texte affiché de l'élément sélectionné
C.Text | texte affiché de l'élément sélectionné

On peut s'étonner qu'un combo puisse contenir des objets alors que visuellement il affiche des chaînes de caractères. Si un ComboBox contient un objet obj , il affiche la chaîne obj.ToString() . On se rappelle que tout objet a une méthode ToString héritée de la classe object et qui rend une chaîne de caractères "représentative" de l'objet.

L'élément Item sélectionné dans le combo C est C.SelectedItem ou C.Items[C.SelectedIndex] où C.SelectedIndex est le n° de l'élément sélectionné, ce n° partant de zéro pour le premier élément. Le texte sélectionné peut être obtenu de diverses façons : C.SelectedItem.Text , C.Text

Lors du choix d'un élément dans la liste déroulante se produit l'événement SelectedIndexChanged qui peut être alors utilisé pour être averti du changement de sélection dans le combo. Dans l'application suivante, nous utilisons cet événement pour afficher l'élément qui a été sélectionné dans la liste.

Image non disponible

Le code de l'application est le suivant :

Cacher Sélectionnez
  1. using System.Windows.Forms;
  2.
  3. namespace Chap5 {
  4.     public partial class Form1 : Form {
  5.         private int previousSelectedIndex=0;
  6.
  7.         public Form1() {
  8.             InitializeComponent();
  9.             // remplissage combo
  10.             comboBoxNombres.Items.AddRange(new string[] { "zéro", "un", "deux", "trois", "quatre" });
  11.             // sélection élément n° 0
  12.             comboBoxNombres.SelectedIndex = 0;
  13.         }
  14.
  15.         private void comboBoxNombres_SelectedIndexChanged(object sender, System.EventArgs e) {
  16.             int newSelectedIndex = comboBoxNombres.SelectedIndex;
  17.             if (newSelectedIndex != previousSelectedIndex) {
  18.                 // l'élément sélectionné à changé - on l'affiche
  19.                 MessageBox.Show(string.Format("Elément sélectionné : ({0},{1})", comboBoxNombres.Text, newSelectedIndex), "Combo", MessageBoxButtons.OK, MessageBoxIcon.Information);
  20.                 // on note le nouvel index
  21.                 previousSelectedIndex = newSelectedIndex;
  22.             }
  23.         }
  24.     }
  25. }
  • ligne 5 : previousSelectedIndex mémorise le dernier index sélectionné dans le combo
  • ligne 10 : remplissage du combo avec un tableau de chaînes de caractères
  • ligne 12 : le 1er élément est sélectionné
  • ligne 15 : la méthode exécutée à chaque fois que l'utilisateur sélectionne un élément du combo. Contrairement à ce que pourrait laisser croire le nom de l'événement, celui-ci a lieu même si l'élément sélectionné est le même que le précédent.
  • ligne 16 : on note l'index de l'élément sélectionné
  • ligne 17 : s'il est différent du précédent
  • ligne 19 : on affiche le n° et le texte de l'élément sélectionné
  • ligne 21 : on note le nouvel index

VIII-B-4. Composant ListBox

On se propose de construire l'interface suivante :

Image non disponible

Les composants de cette fenêtre sont les suivants :

n° | type | nom | rôle/propriétés
0 | Form | Form1 | formulaire FormBorderStyle =FixedSingle (cadre non redimensionable)
1 | TextBox | textBoxSaisie | champ de saisie
2 | Button | buttonAjouter | bouton permettant d'ajouter le contenu du champ de saisie [1] dans la liste [3]
3 | ListBox | listBox1 | liste 1 SelectionMode =MultiExtended :
4 | ListBox | listBox2 | liste 2 SelectionMode =MultiSimple :
5 | Button | button1vers2 | transfère les éléments sélectionnés de liste 1 vers liste 2
6 | Button | button2vers1 | fait l'inverse
7 | Button | buttonEffacer1 | vide la liste 1
8 | Button | buttonEffacer2 | vide la liste 2

Les composants ListBox ont un mode de sélection de leurs éléments qui est défini par leur propriété SelectionMode :

One | un seul élément peut être sélectionné
MultiExtended | multisélection possible : maintenir appuyée la touche SHIFT et cliquer sur un élément étend la sélection de l'élément précédemment sélectionné à l'élément courant.
MultiSimple | multisélection possible : un élément est sélectionné / désélectionné par un clic de souris ou par appui sur la barre d'espace.
  • L'utilisateur tape du texte dans le champ 1. Il l'ajoute à la liste 1 avec le bouton Ajouter (2). Le champ de saisie (1) est alors vidé et l'utilisateur peut ajouter un nouvel élément.
  • Il peut transférer des éléments d'une liste à l'autre en sélectionnant l'élément à transférer dans l'une des listes et en choississant le bouton de transfert adéquat 5 ou 6. L'élément transféré est ajouté à la fin de la liste de destination et enlevé de la liste source.
  • Il peut double-cliquer sur un élément de la liste 1. Cet élément est alors transféré dans la boîte de saisie pour modification et enlevé de la liste 1.

Les boutons sont allumés ou éteints selon les règles suivantes :

  • le bouton Ajouter n'est allumé que s'il y a un texte non vide dans le champ de saisie
  • le bouton [5] de transfert de la liste 1 vers la liste 2 n'est allumé que s'il y a un élément sélectionné dans la liste 1
  • le bouton [6] de transfert de la liste 2 vers la liste 1 n'est allumé que s'il y a un élément sélectionné dans la liste 2
  • les boutons [7] et [8] d'effacement des listes 1 et 2 ne sont allumés que si la liste à effacer contient des éléments.

Dans les conditions précédentes, tous les boutons doivent être éteints lors du démarrage de l'application. C'est la propriété Enabled des boutons qu'il faut alors positionner à false . On peut le faire au moment de la conception ce qui aura pour effet de générer le code correspondant dans la méthode InitializeComponent ou le faire nous-mêmes dans le constructeur comme ci-dessous :

Cacher Sélectionnez
  1.         public Form1() {
  2.             InitializeComponent();
  3.             // --- initialisations complémentaires ---
  4.             // on inhibe un certain nombre de boutons
  5.             buttonAjouter.Enabled = false;
  6.             button1vers2.Enabled = false;
  7.             button2vers1.Enabled = false;
  8.             buttonEffacer1.Enabled = false;
  9.             buttonEffacer2.Enabled = false;
  10. }

L'état du bouton Ajouter est contrôlé par le contenu du champ de saisie. C'est l'événement TextChanged qui nous permet de suivre les changements de ce contenu :

Cacher Sélectionnez
  1.         private void textBoxSaisie_TextChanged(object sender, System.EventArgs e) {
  2.             // le contenu de textBoxSaisie a changé
  3.             // le bouton Ajouter n'est allumé que si la saisie est non vide
  4.             buttonAjouter.Enabled = textBoxSaisie.Text.Trim() != "";
  5.         }

L'état des boutons de transfert dépend du fait qu'un élément a été sélectionné ou non dans la liste qu'ils contrôlent :

Cacher Sélectionnez
  1.         private void listBox1_SelectedIndexChanged(object sender, System.EventArgs e) {
  2.             // un élément a été sélectionné
  3.             // on allume le bouton de transfert 1 vers 2
  4.             button1vers2.Enabled = true;
  5.         }
  6.
  7.         private void listBox2_SelectedIndexChanged(object sender, System.EventArgs e) {
  8.             // un élément a été sélectionné
  9.             // on allume le bouton de transfert 2 vers 1
  10.             button2vers1.Enabled = true;
  11. }

Le code associé au clic sur le bouton Ajouter est le suivant :

Cacher Sélectionnez
  1.         private void buttonAjouter_Click(object sender, System.EventArgs e) {
  2.             // ajout d'un nouvel élément à la liste 1
  3.             listBox1.Items.Add(textBoxSaisie.Text.Trim());
  4.             // raz de la saisie
  5.             textBoxSaisie.Text = "";
  6.             // Liste 1 n'est pas vide
  7.             buttonEffacer1.Enabled = true;
  8.             // retour du focus sur la boîte de saisie
  9.             textBoxSaisie.Focus();
  10. }

On notera la méthode Focus qui permet de mettre le "focus" sur un contrôle du formulaire. Le code associé au clic sur les boutons Effacer :

Cacher Sélectionnez
  1.         private void buttonEffacer1_Click(object sender, System.EventArgs e) {
  2.             // on efface la liste 1
  3.             listBox1.Items.Clear();
  4.             // bouton Effacer
  5.             buttonEffacer1.Enabled = false;
  6.         }
  7.
  8.         private void buttonEffacer2_Click(object sender, System.EventArgs e) {
  9.             // on efface la liste 2
  10.             listBox2.Items.Clear();
  11.             // bouton Effacer
  12.             buttonEffacer2.Enabled = false;
  13. }

Le code de transfert des éléments sélectionnés d'une liste vers l'autre :

Cacher Sélectionnez
  1.         private void button1vers2_Click(object sender, System.EventArgs e) {
  2.             // transfert de l'élément sélectionné dans Liste 1 dans Liste 2
  3.             transfert(listBox1, button1vers2, buttonEffacer1, listBox2, button2vers1, buttonEffacer2);
  4.         }
  5.
  6.         private void button2vers1_Click(object sender, System.EventArgs e) {
  7.             // transfert de l'élément sélectionné dans Liste 2 dans Liste 1
  8.             transfert(listBox2, button2vers1, buttonEffacer2, listBox1, button1vers2, buttonEffacer1);
  9.         }

Les deux méthodes ci-dessus délèguent le transfert des éléments sélectionnés d'une liste à l'autre à une même méthode privée appelée transfert :

Cacher Sélectionnez
  1.         // transfert
  2.         private void transfert(ListBox l1, Button button1vers2, Button buttonEffacer1, ListBox l2, Button button2vers1, Button buttonEffacer2) {
  3.             // transfert dans la liste l2 des éléments sélectionnés de la liste l1
  4.             for (int i = l1.SelectedIndices.Count - 1; i >= 0; i--) {
  5.                 // index de l'élément sélectionné
  6.                 int index = l1.SelectedIndices[i];
  7.                 // ajout dans l2
  8.                 l2.Items.Add(l1.Items[index]);
  9.                 // suppression dans l1
  10.                 l1.Items.RemoveAt(index);
  11.             }
  12.             // boutons Effacer
  13.             buttonEffacer2.Enabled = l2.Items.Count != 0;
  14.             buttonEffacer1.Enabled = l1.Items.Count != 0;
  15.             // boutons de transfert
  16.             button1vers2.Enabled = false;
  17. }
  • ligne b : la méthode transfert reçoit six paramètres :
  • une référence sur la liste contenant les éléments sélectionnés appelée ici l1. Lors de l'exécution de l'application, l1 est soit listBox1 soit listBox2 . On voit des exemples d'appel, lignes 3 et 8 des procédures de transfert buttonXversY_Click .
  • une référence sur le bouton de transfert lié à la liste l1. Par exemple si l1 est listBox2 , ce sera button2vers1 ( cf appel ligne 8)
  • une référence sur le bouton d'effacement de la liste l1. Par exemple si l1 est listBox1 , ce sera buttonEffacer1 ( cf appel ligne 3)
  • les trois autres références sont analogues mais font référence à la liste l2.
  • ligne d : la collection [ListBox].SelectedIndices représente les indices des éléments sélectionnés dans le composant [ListBox]. C'est une collection :
  • [ListBox].SelectedIndices.Count est le nombre d'élément de cette collection
  • [ListBox].SelectedIndices[i] est l'élément n° i de cette collection

On parcourt la collection en sens inverse : on commence par la fin de la collection pour terminer par le début. Nous expliquerons pourquoi.

  • ligne f : indice d'un élément sélectionné de la liste l1
  • ligne h : cet élément est ajouté dans la liste l2
  • ligne j : et supprimé de la liste l1. Parce qu'il est supprimé, il n'est plus sélectionné. La collection l1.SelectedIndices de la ligne d va être recalculée. Elle va perdre l'élément qui vient d'être supprimé. Tous les éléments qui sont après celui-ci vont voir leur n° passer de n à n-1.
  • si la boucle de la ligne (d) est croissante et qu'elle vient de traiter l'élément n° 0, elle va ensuite traiter l'élément n° 1. Or l'élément qui portait le n° 1 avant la suppression de l'élément n° 0, va ensuite porter le n° 0. Il sera alors oublié par la boucle.
  • si la boucle de la ligne (d) est décroissante et qu'elle vient de traiter l'élément n° n, elle va ensuite traiter l'élément n° n-1. Après suppression de l'élément n° n, l'élément n° n-1 ne change pas de n°. Il est donc traité au tour de boucle suivant.
  • lignes m-n : l'état des boutons [Effacer] dépend de la présence ou non d'éléments dans les listes associées
  • ligne p : la liste l2 n'a plus d'éléments sélectionnés : on éteint son bouton de transfert.

VIII-B-5. Cases à cocher CheckBox, boutons radio ButtonRadio

Nous nous proposons d'écrire l'application suivante :

Image non disponible

Les composants de la fenêtre sont les suivants :

n° | type | nom | rôle
1 | GroupBox cf [6] | groupBox1 | un conteneur de composants. On peut y déposer d'autres composants. Text =Boutons radio
2 | RadioButton | radioButton1 radioButton2 radioButton3 | 3 boutons radio - radioButton1 a la propriété Checked =True et la propriété Text =1 - radioButton2 a la propriété Text =2 - radioButton3 a la propriété Text =3 Des boutons radio présents dans un même conteneur, ici le GroupBox , sont exclusifs l'un de l'autre : seul l'un d'entre-eux est allumé.
3 | GroupBox | groupBox2 |
4 | CheckBox | checkBox1 checkBox2 checkBox3 | 3 cases à cocher. chechBox1 a la propriété Checked =True et la propriété Text =A - chechBox2 a la propriété Text =B - chechBox3 a la propriété Text =C
5 | ListBox | listBoxValeurs | une liste qui affiche les valeurs des boutons radio et des cases à cocher dès qu'un changement intervient.
6 |  |  | montre où trouver le conteneur GroupBox

L'événement qui nous intéresse pour ces six contrôles est l'événement CheckChanged indiquant que l'état de la case à cocher ou du bouton radio a changé. Cet état est représenté dans les deux cas par la propriété booléenne Checked qui à vrai signifie que le contrôle est coché. Nous n'utiliserons ici qu'une seule méthode pour traiter les six événements CheckChanged , la méthode affiche . Pour faire en sorte que les six événements CheckChanged soient gérés par la même méthode affiche , on pourra procéder comme suit :

le composant radioButton1 et cliquons droit dessus pour avoir accès à ses propriétés :

Image non disponible

Dans l'onglet événements [1], on associe la méthode affiche [2] à l'événement CheckChanged . Cela signifie que l'on souhaite que le clic sur l'option A1 soit traitée par une méthode appelée affiche . Visual studio génère automatiquement la méthode affiche dans la fenêtre de code :

Cacher Sélectionnez
  1. private void affiche(object sender, EventArgs e) {
  2.         }

La méthode affiche est une méthode de type EventHandler .

Pour les cinq autres composants, on procède de même. Sélectionnons par exemple l'option CheckBox1 et ses événements [3]. En face de l'événement Click , on a une liste déroulante [4] dans laquelle sont présentes les méthodes existantes pouvant traiter cet événement. Ici on n'a que la méthode affiche. On la sélectionne. On répète ce processus pour tous les autres composants.

Dans la méthode InitializeComponent du code a été généré. La méthode affiche a été déclarée comme gestionnaire des six événements CheckedChanged de la façon suivante :

Cacher Sélectionnez
  1. this.radioButton1.CheckedChanged += new System.EventHandler(this.affiche);
  2. this.radioButton2.CheckedChanged += new System.EventHandler(this.affiche);
  3. this.radioButton3.CheckedChanged += new System.EventHandler(this.affiche);
  4. this.checkBox1.CheckedChanged += new System.EventHandler(this.affiche);
  5. this.checkBox2.CheckedChanged += new System.EventHandler(this.affiche);
  6. this.checkBox3.CheckedChanged += new System.EventHandler(this.affiche);

La méthode affiche est complétée comme suit :

Cacher Sélectionnez
  1.         private void affiche(object sender, System.EventArgs e) {
  2.             // affiche l'état du bouton radio ou de la case à cocher
  3.             // est-ce un checkbox ?
  4.             if (sender is CheckBox) {
  5.                 CheckBox chk = (CheckBox)sender;
  6.                 listBoxvaleurs.Items.Add(chk.Name + "=" + chk.Checked);
  7.             }
  8.             // est-ce un radiobutton ?
  9.             if (sender is RadioButton) {
  10.                 RadioButton rdb = (RadioButton)sender;
  11.                 listBoxvaleurs.Items.Add(rdb.Name + "=" + rdb.Checked);
  12.             }
  13. }

La syntaxe

Cacher Sélectionnez
  1.             if (sender is CheckBox) {

permet de vérifier que l'objet sender est de type CheckBox . Cela nous permet ensuite de faire un transtypage vers le type exact de sender . La méthode affiche écrit dans la liste listBoxValeurs le nom du composant à l'origine de l'événement et la valeur de sa propriété Checked . A l'exécution [7], on voit qu'un clic sur un bouton radio provoque deux événements CheckChanged : l'un sur l'ancien bouton coché qui passe à "non coché" et l'autre sur le nouveau bouton qui passe à "coché".

VIII-B-6. Variateurs ScrollBar

Il existe plusieurs types de variateur : le variateur horizontal (H ScrollBar ), le variateur vertical (V ScrollBar ), l'incrémenteur ( NumericUpDown ). Image non disponible Image non disponible Image non disponible

Réalisons l'application suivante :

Image non disponible
1234
n° | type | nom | rôle
1 | hScrollBar | hScrollBar1 | un variateur horizontal
2 | hScrollBar | hScrollBar2 | un variateur horizontal qui suit les variations du variateur 1
3 | Label | labelValeurHS1 | affiche la valeur du variateur horizontal
4 | NumericUpDown | numericUpDown2 | permet de fixer la valeur du variateur 2
  • Un variateur ScrollBar permet à l'utilisateur de choisir une valeur dans une plage de valeurs entières symbolisée par la "bande" du variateur sur laquelle se déplace un curseur. La valeur du variateur est disponible dans sa propriété Value.
  • Pour un variateur horizontal, l'extrémité gauche représente la valeur minimale de la plage, l'extrémité droite la valeur maximale, le curseur la valeur actuelle choisie. Pour un variateur vertical, le minimum est représenté par l'extrémité haute, le maximum par l'extrémité basse. Ces valeurs sont représentées par les propriétés Minimum et Maximum et valent par défaut 0 et 100.
  • Un clic sur les extrémités du variateur fait varier la valeur d'un incrément (positif ou négatif) selon l'extrémité cliquée appelée SmallChange qui vaut par défaut 1.
  • Un clic de part et d'autre du curseur fait varier la valeur d'un incrément (positif ou négatif) selon l'extrémité cliquée appelée LargeChange qui vaut par défaut 10.
  • Lorsqu'on clique sur l'extrémité supérieure d'un variateur vertical, sa valeur diminue. Cela peut surprendre l'utilisateur moyen qui s'attend normalement à voir la valeur "monter". On règle ce problème en donnant une valeur négative aux propriétés SmallChange et LargeChange
  • Ces cinq propriétés (Value, Minimum, Maximum, SmallChange, LargeChange) sont accessibles en lecture et écriture.
  • L'événement principal du variateur est celui qui signale un changement de valeur : l'événement Scroll.

Un composant NumericUpDown est proche du variateur : il a lui aussi les propriétés Minimum , Maximum et Value , par défaut 0, 100, 0. Mais ici, la propriété Value est affichée dans une boîte de saisie faisant partie intégrante du contrôle. L'utilisateur peut lui même modifier cette valeur sauf si on a mis la propriété ReadOnly du contrôle à vrai. La valeur de l'incrément est fixée par la propriété Increment , par défaut 1. L'événement principal du composant NumericUpDown est celui qui signale un changement de valeur : l'événement ValueChanged

Le code de l'application est le suivant :

Cacher Sélectionnez
  1. using System.Windows.Forms;
  2.
  3. namespace Chap5 {
  4.     public partial class Form1 : Form {
  5.         public Form1() {
  6.             InitializeComponent();
  7.             // on fixe les caractéristiques du variateur 1
  8.             hScrollBar1.Value = 7;
  9.             hScrollBar1.Minimum = 1;
  10.             hScrollBar1.Maximum = 130;
  11.             hScrollBar1.LargeChange = 11;
  12.             hScrollBar1.SmallChange = 1;
  13.             // on donne au variateur 2 les mêmes caractéristiques qu'au variateur 1
  14.             hScrollBar2.Value = hScrollBar1.Value;
  15.             hScrollBar2.Minimum = hScrollBar1.Minimum;
  16.             hScrollBar2.Maximum = hScrollBar1.Maximum;
  17.             hScrollBar2.LargeChange = hScrollBar1.LargeChange;
  18.             hScrollBar2.SmallChange = hScrollBar1.SmallChange;
  19.             // idem pour l'incrémenteur
  20.             numericUpDown2.Value = hScrollBar1.Value;
  21.             numericUpDown2.Minimum = hScrollBar1.Minimum;
  22.             numericUpDown2.Maximum = hScrollBar1.Maximum;
  23.             numericUpDown2.Increment = hScrollBar1.SmallChange;
  24.
  25.             // on donne au Label la valeur du variateur 1
  26.             labelValeurHS1.Text = hScrollBar1.Value.ToString();
  27.         }
  28.
  29.         private void hScrollBar1_Scroll(object sender, ScrollEventArgs e) {
  30.             // changement de valeur du variateur 1
  31.             // on répercute sa valeur sur le variateur 2 et sur le label
  32.             hScrollBar2.Value = hScrollBar1.Value;
  33.             labelValeurHS1.Text = hScrollBar1.Value.ToString();
  34.         }
  35.
  36.         private void numericUpDown2_ValueChanged(object sender, System.EventArgs e) {
  37.             // l'incrémenteur a changé de valeur
  38.             // on fixe la valeur du variateur 2
  39.             hScrollBar2.Value = (int)numericUpDown2.Value;
  40.         }
  41.     }
  42. }

VIII-C. Événements souris

Lorsqu'on dessine dans un conteneur, il est important de connaître la position de la souris pour, par exemple, afficher un point lors d'un clic. Les déplacements de la souris provoquent des événements dans le conteneur dans lequel elle se déplace.

Image non disponible
  • [1] : les événements survenant lors d'un déplacement de la souris sur le formulaire ou sur un contrôle
  • [2] : les événements survenant lors d'un glisser / lâcher (Drag'nDrop)
MouseEnter | la souris vient d'entrer dans le domaine du contrôle
MouseLeave | la souris vient de quitter le domaine du contrôle
MouseMove | la souris bouge dans le domaine du contrôle
MouseDown | Pression sur le bouton gauche de la souris
MouseUp | Relâchement du bouton gauche de la souris
DragDrop | l'utilisateur lâche un objet sur le contrôle
DragEnter | l'utilisateur entre dans le domaine du contrôle en tirant un objet
DragLeave | l'utilisateur sort du domaine du contrôle en tirant un objet
DragOver | l'utilisateur passe au-dessus domaine du contrôle en tirant un objet

Voici une application permettant de mieux appréhender à quels moments se produisent les différents événements souris :

Image non disponible
n° | type | nom | rôle
1 | Label | lblPositionSouris | pour afficher la position de la souris dans le formulaire 1, la liste 2 ou le bouton 3
2 | ListBox | listBoxEvts | pour afficher les évts souris autres que MouseMove
3 | Button | buttonEffacer | pour effacer le contenu de 2

Pour suivre les déplacements de la souris sur les trois contrôles, on n'écrit qu'un seul gestionnaire, le gestionnaire affiche :

Image non disponible

Le code de la procédure affiche est le suivant :

Cacher Sélectionnez
  1.         private void affiche(object sender, MouseEventArgs e) {
  2.             // mvt souris - on affiche les coordonnées (X,Y) de celle-ci
  3.             labelPositionSouris.Text = "(" + e.X + "," + e.Y + ")";
  4. }

A chaque fois que la souris entre dans le domaine d'un contrôle son système de coordonnées change. Son origine (0,0) est le coin supérieur gauche du contrôle sur lequel elle se trouve. Ainsi à l'exécution, lorsqu'on passe la souris du formulaire au bouton, on voit clairement le changement de coordonnées. Afin de mieux voir ces changements de domaine de la souris, on peut utiliser la propriété Cursor [1] des contrôles :

Image non disponible

Cette propriété permet de fixer la forme du curseur de souris lorsque celle-ci entre dans le domaine du contrôle. Ainsi dans notre exemple, nous avons fixé le curseur à Default pour le formulaire lui-même [2], Hand pour la liste 2 [3] et à Cross pour le bouton 3 [4].

Par ailleurs, pour détecter les entrées et sorties de la souris sur la liste 2, nous traitons les événements MouseEnter et MouseLeave de cette même liste :

Cacher Sélectionnez
  1.         private void listBoxEvts_MouseEnter(object sender, System.EventArgs e) {
  2.             // on signale l'évt
  3.             listBoxEvts.Items.Insert(0, string.Format("MouseEnter à {0:hh:mm:ss}",DateTime.Now));
  4.         }
  5.
  6.         private void listBoxEvts_MouseLeave(object sender, EventArgs e) {
  7.             // on signale l'évt
  8.             listBoxEvts.Items.Insert(0, string.Format("MouseLeave à {0:hh:mm:ss}", DateTime.Now));
  9. }

Pour traiter les clics sur le formulaire, nous traitons les événements MouseDown et MouseUp :

Cacher Sélectionnez
  1.         private void listBoxEvts_MouseDown(object sender, MouseEventArgs e) {
  2.             // on signale l'évt
  3.             listBoxEvts.Items.Insert(0, string.Format("MouseDown à {0:hh:mm:ss}", DateTime.Now));
  4.         }
  5.
  6.         private void listBoxEvts_MouseUp(object sender, MouseEventArgs e) {
  7.             // on signale l'évt
  8.             listBoxEvts.Items.Insert(0, string.Format("MouseUp à {0:hh:mm:ss}", DateTime.Now));
  9. }
  • lignes 3 et 8 : les messages sont placés en 1re position dans le ListBox afin que les événements les plus récents soient les premiers dans la liste.
Image non disponible

Enfin, le code du gestionnaire de clic sur le bouton Effacer :

Cacher Sélectionnez
  1.         private void buttonEffacer_Click(object sender, EventArgs e) {
  2.             listBoxEvts.Items.Clear();
  3. }

VIII-D. Créer une fenêtre avec menu

Voyons maintenant comment créer une fenêtre avec menu. Nous allons créer la fenêtre suivante :

Image non disponible

Pour créer un menu, on choisit le composant " MenuStrip " dans la barre " Menus & Tollbars " :

Image non disponible
  • [1] : choix du composant [MenuStrip]
  • [2] : on a alors un menu qui s'installe sur le formulaire avec des cases vides intitulées "Type Here". Il suffit d'y indiquer les différentes options du menu.
  • [3] : le libellé "Options A" a été tapé. On passe au libellé [4].
  • [5] : les libellés des options A ont été saisis. On passe au libellé [6]
Image non disponible
  • [6] : les premières options B
  • [7] : sous B1, on met un séparateur. Celui-ci est disponible dans un combo associé au texte "Type Here"
  • [8] : pour faire un sous-menu, utiliser la flèche [8] et taper le sous-menu dans [9]

Il reste à nommer le différents composants du formulaire :

Image non disponible
n° | type | nom(s) | rôle
1 | Label | labelStatut | pour afficher le texte de l'option de menu cliquée
2 | toolStripMenuItem | toolStripMenuItemOptionsA toolStripMenuItemA1 toolStripMenuItemA2 toolStripMenuItemA3 | options de menu sous l'option principale "Options A"
3 | toolStripMenuItem | toolStripMenuItemOptionsB toolStripMenuItemB1 toolStripMenuItemB2 toolStripMenuItemB3 | options de menu sous l'option principale "Options B"
4 | toolStripMenuItem | toolStripMenuItemB31 toolStripMenuItemB32 | options de menu sous l'option principale "B3"

Les options de menu sont des contrôles comme les autres composants visuels et ont des propriétés et événements. Par exemple les propriétés de l'option de menu A1 sont les suivantes :

Image non disponible

Deux propriétés sont utilisées dans notre exemple :

Name | le nom du contrôle menu
Text | le libellé de l'option de menu

Dans la structure du menu, sélectionnons l'option A1 et cliquons droit pour avoir accès aux propriétés du contrôle :

Image non disponible

Dans l'onglet événements [1], on associe la méthode affiche [2] à l'événement Click . Cela signifie que l'on souhaite que le clic sur l'option A1 soit traitée par une méthode appelée affiche . Visual studio génère automatiquement la méthode affiche dans la fenêtre de code :

Cacher Sélectionnez
  1. private void affiche(object sender, EventArgs e) {
  2.         }

Dans cette méthode, nous nous contenterons d'afficher dans le label labelStatut la propriété Text de l'option de menu qui a été cliquée :

Cacher Sélectionnez
  1. private void affiche(object sender, EventArgs e) {
  2.             // affiche dans le TextBox le nom du sous-menu choisi
  3.             labelStatut.Text = ((ToolStripMenuItem)sender).Text;
  4. }

La source de l'événement sender est de type object . Les options de menu sont elle de type ToolStripMenuItem , aussi est-on obligé de faire un transtypage de object vers ToolStripMenuItem .

Pour toutes les options de menu, on fixe le gestionnaire du clic à la méthode affiche [3,4].

Exécutons l'application et sélectionnons un élément de menu :

Image non disponible

VIII-E. Composants non visuels

Nous nous intéressons maintenant à un certain nombre de composants non visuels : on les utilise lors de la conception mais on ne les voit pas lors de l'exécution.

VIII-E-1. Boîtes de dialogue OpenFileDialog et SaveFileDialog

Nous allons construire l'application suivante :

Image non disponible

Les contrôles sont les suivants :

N° | type | nom | rôle
1 | TextBox | TextBoxLignes | texte tapé par l'utilisateur ou chargé à partir d'un fichier MultiLine =True, ScrollBars =Both, AccepReturn =True, AcceptTab =True
2 | Button | buttonSauvegarder | permet de sauvegarder le texte de [1] dans un fichier texte
3 | Button | buttonCharger | permet de charger le contenu d'un fichier texte dans [1]
4 | Button | buttonEffacer | efface le contenu de [1]
5 | SaveFileDialog | saveFileDialog1 | composant permettant de choisir le nom et l'emplacement du fichier de sauvegarde de [1]. Ce composant est pris dans la barre d'outils [7] et simplement déposé sur le formulaire. Il est alors enregistré mais il n'occupe pas de place sur le formulaire. C'est un composant non visuel.
6 | OpenFileDialog | openFileDialog1 | composant permettant de choisir le fichier à charger dans [1].

Le code associé au bouton Effacer est simple :

Cacher Sélectionnez
  1.         private void buttonEffacer_Click(object sender, EventArgs e) {
  2.             // on met la chaîne vide dans le TexBox
  3.             textBoxLignes.Text = "";
  4. }

Nous utiliserons les propriétés et méthodes suivantes de la classe SaveFileDialog :

Champ | Type | Rôle
string Filter | Propriété | les types de fichiers proposés dans la liste déroulante des types de fichiers de la boîte de dialogue
int FilterIndex | Propriété | le n° du type de fichier proposé par défaut dans la liste ci-dessus. Commence à 0.
string InitialDirectory | Propriété | le dossier présenté initialement pour la sauvegarde du fichier
string FileName | Propriété | le nom du fichier de sauvegarde indiqué par l'utilisateur
DialogResult.ShowDialog() | Méthode | méthode qui affiche la boîte de dialogue de sauvegarde. Rend un résultat de type DialogResult .

La méthode ShowDialog affiche une boîte de dialogue analogue à la suivante :

Image non disponible
1 | liste déroulante construite à partir de la propriété Filter. Le type de fichier proposé par défaut est fixé par FilterIndex
2 | dossier courant, fixé par InitialDirectory si cette propriété a été renseignée
3 | nom du fichier choisi ou tapé directement par l'utilisateur. Sera disponible dans la propriété FileName
4 | boutons Enregistrer/Annuler. Si le bouton Enregistrer est utilisé, la fonction ShowDialog rend le résultat DialogResult.OK

La procédure de sauvegarde peut s'écrire ainsi :

Cacher Sélectionnez
  1. private void buttonSauvegarder_Click(object sender, System.EventArgs e) {
  2.             // on sauvegarde la boîte de saisie dans un fichier texte
  3.             // on paramètre la boîte de dialogue savefileDialog1
  4.             saveFileDialog1.InitialDirectory = Application.ExecutablePath;
  5.             saveFileDialog1.Filter = "Fichiers texte (*.txt)|*.txt|Tous les fichiers (*.*)|*.*";
  6.             saveFileDialog1.FilterIndex = 0;
  7.             // on affiche la boîte de dialogue et on récupère son résultat
  8.             if (saveFileDialog1.ShowDialog() == DialogResult.OK) {
  9.                 // on récupère le nom du fichier
  10.                 string nomFichier = saveFileDialog1.FileName;
  11.                 StreamWriter fichier = null;
  12.                 try {
  13.                     // on ouvre le fichier en écriture
  14.                     fichier = new StreamWriter(nomFichier);
  15.                     // on écrit le texte dedans
  16.                     fichier.Write(textBoxLignes.Text);
  17.                 } catch (Exception ex) {
  18.                     // problème
  19.                     MessageBox.Show("Problème à l'écriture du fichier (" +
  20.                     ex.Message + ")", "Erreur", MessageBoxButtons.OK, MessageBoxIcon.Error);
  21.                     return;
  22.                 } finally {
  23.                     // on ferme le fichier
  24.                     if (fichier != null) {
  25.                         fichier.Dispose();
  26.                     }
  27.                 }
  28.             }
  29.         }
  • ligne 4 : on fixe le dossier initial ( InitialDirectory ) au dossier ( Application.ExecutablePath ) qui contient l'exécutable de l'application.
  • ligne 5 : on fixe les types de fichiers à présenter. On notera la syntaxe des filtres : filtre1 | filtre2 |..| filtren avec filtrei= Texte|modèle de fichier . Ici l'utilisateur aura le choix entre les fichiers *.txt et *.* .
  • ligne 6 : on fixe le type de fichier à présenter en premier à l'utilisateur. Ici l'index 0 désigne les fichiers *.txt.
  • ligne 8 : la boîte de dialogue est affichée et son résultat récupéré. Pendant que la boîte de dialogue est affichée, l'utilisateur n'a plus accès au formulaire principal (boîte de dialogue dite modale). L'utilisateur fixe le nom du fichier à sauvegarder et quitte la boîte soit par le bouton Enregistrer , soit par le bouton Annuler, soit en fermant la boîte. Le résultat de la méthode ShowDialog est DialogResult.OK uniquement si l'utilisateur a utilisé le bouton Enregistrer pour quitter la boîte de dialogue.
  • Ceci fait, le nom du fichier à créer est maintenant dans la propriété FileName de l'objet saveFileDialog1 . On est alors ramené à la création classique d'un fichier texte. On y écrit le contenu du TextBox : textBoxLignes.Text tout en gérant les exceptions qui peuvent se produire.

La classe OpenFileDialog est très proche de la classe SaveFileDialog . On utilisera les mêmes méthodes et propriétés que précédemment. La méthode ShowDialog affiche une boîte de dialogue analogue à la suivante :

Image non disponible
1 | liste déroulante construite à partir de la propriété Filter. Le type de fichier proposé par défaut est fixé par FilterIndex
2 | dossier courant, fixé par InitialDirectory si cette propriété a été renseignée
3 | nom du fichier choisi ou tapé directement par l'utilisateur. Sera disponible dans la propriété FileName
4 | boutons Ouvrir/Annuler. Si le bouton Ouvrir est utilisé, la fonction ShowDialog rend le résultat DialogResult.OK

La procédure de chargement du fichier texte peut s'écrire ainsi :

Cacher Sélectionnez
  1. private void buttonCharger_Click(object sender, EventArgs e) {
  2.             // on charge un fichier texte dans la boîte de saisie
  3.             // on paramètre la boîte de dialogue openfileDialog1
  4.             openFileDialog1.InitialDirectory = Application.ExecutablePath;
  5.             openFileDialog1.Filter = "Fichiers texte (*.txt)|*.txt|Tous les fichiers (*.*)|*.*";
  6.             openFileDialog1.FilterIndex = 0;
  7.             // on affiche la boîte de dialogue et on récupère son résultat
  8.             if (openFileDialog1.ShowDialog() == DialogResult.OK) {
  9.                 // on récupère le nom du fichier
  10.                 string nomFichier = openFileDialog1.FileName;
  11.                 StreamReader fichier = null;
  12.                 try {
  13.                     // on ouvre le fichier en lecture
  14.                     fichier = new StreamReader(nomFichier);
  15.                     // on lit tout le fichier et on le met dans le TextBox
  16.                     textBoxLignes.Text = fichier.ReadToEnd();
  17.                 } catch (Exception ex) {
  18.                     // problème
  19.                     MessageBox.Show("Problème à la lecture du fichier (" +
  20.                     ex.Message + ")", "Erreur", MessageBoxButtons.OK, MessageBoxIcon.Error);
  21.                     return;
  22.                 } finally {
  23.                     // on ferme le fichier
  24.                     if (fichier != null) {
  25.                         fichier.Dispose();
  26.                     }
  27.                 }//finally
  28.             }//if
  29.         }
  • ligne 4 : on fixe le dossier initial ( InitialDirectory ) au dossier ( Application.ExecutablePath ) qui contient l'exécutable de l'application.
  • ligne 5 : on fixe les types de fichiers à présenter. On notera la syntaxe des filtres : filtre1 | filtre2 |..| filtren avec filtrei= Texte|modèle de fichier . Ici l'utilisateur aura le choix entre les fichiers *.txt et *.* .
  • ligne 6 : on fixe le type de fichier à présenter en premier à l'utilisateur. Ici l'index 0 désigne les fichiers *.txt.
  • ligne 8 : la boîte de dialogue est affichée et son résultat récupéré. Pendant que la boîte de dialogue est affichée, l'utilisateur n'a plus accès au formulaire principal (boîte de dialogue dite modale). L'utilisateur fixe le nom du fichier à sauvegarder et quitte la boîte soit par le bouton Ouvrir , soit par le bouton Annuler, soit en fermant la boîte. Le résultat de la méthode ShowDialog est DialogResult.OK uniquement si l'utilisateur a utilisé le bouton Enregistrer pour quitter la boîte de dialogue.

VIII-E-2. Boîtes de dialogue FontColor et ColorDialog

Nous continuons l'exemple précédent en y ajoutant deux nouveaux boutons et deux nouveaux contrôles non visuels :

Image non disponible
N° | type | nom | rôle
1 | Button | buttonCouleur | pour fixer la couleur des caractères du TextBox
2 | Button | buttonPolice | pour fixer la police de caractères du TextBox
3 | ColorDialog | colorDialog1 | le composant qui permet la sélection d'une couleur - pris dans la boîte à outils [5].
4 | FontDialog | colorDialog1 | le composant qui permet la sélection d'une police de caractères - pris dans la boîte à outils [5].

Les classes FontDialog et ColorDialog ont une méthode ShowDialog analogue à la méthode ShowDialog des classes OpenFileDialog et SaveFileDialog .

La méthode ShowDialog de la classe ColorDialog permet de choisir une couleur [1]. Celle de la classe FontDialog permet de choisir une police de caractères [2] :

Image non disponible
  • [1] : si l'utilisateur quitte la boîte de dialogue avec le bouton OK , le résultat de la méthode ShowDialog est DialogResult.OK et la couleur choisie est dans la propriété Color de l'objet ColorDialog utilisé.
  • [2] : si l'utilisateur quitte la boîte de dialogue avec le bouton OK , le résultat de la méthode ShowDialog est DialogResult.OK et la police choisie est dans la propriété Font de l'objet FontDialog utilisé.

Nous avons désormais les éléments pour traiter les clics sur les boutons Couleur et Police :

Cacher Sélectionnez
  1.         private void buttonCouleur_Click(object sender, EventArgs e) {// choix d'une couleur de texte
  2.             if (colorDialog1.ShowDialog() == DialogResult.OK) {
  3.                 // on change la propriété Forecolor du TextBox
  4.                 textBoxLignes.ForeColor = colorDialog1.Color;
  5.             }//if
  6.         }
  7.
  8.         private void buttonPolice_Click(object sender, EventArgs e) {
  9.             // choix d'une police de caractères
  10.             if (fontDialog1.ShowDialog() == DialogResult.OK) {
  11.                 // on change la propriété Font du TextBox
  12.                 textBoxLignes.Font = fontDialog1.Font;
  13. }
  • ligne [4] : la propriété [ForeColor] d'un composant TextBox désigne la couleur de type [Color] des caractères du TextBox . Ici cette couleur est celle choisie par l'utilisateur dans la boîte de dialogue de type [ColorDialog].
  • ligne [12] : la propriété [Font] d'un composant TextBox désigne la police de caractères de type [Font] des caractères du TextBox . Ici cette police est celle choisie par l'utilisateur dans la boîte de dialogue de type [FontDialog].

VIII-E-3. Timer

Nous nous proposons ici d'écrire l'application suivante :

Image non disponible
n° | Type | Nom | Rôle
1 | Label | labelChrono | affiche un chronomètre
2 | Button | buttonArretMarche | bouton Arrêt/Marche du chronomètre
3 | Timer | timer1 | composant émettant ici un événement toutes les secondes

En [4], nous voyons le chronomètre en marche, en [5] le chronomètre arrêté.

Pour changer toutes les secondes le contenu du Label LabelChrono , il nous faut un composant qui génère un événement toutes les secondes, événement qu'on pourra intercepter pour mettre à jour l'affichage du chronomètre. Ce composant c'est le Timer [1] disponible dans la boîte à outils Components [2] :

Image non disponible

Les propriétés du composant Timer utilisées ici seront les suivantes :

Interval | nombre de millisecondes au bout duquel un événement Tick est émis.
Tick | l'événement produit à la fin de Interval millisecondes
Enabled | rend le timer actif (true) ou inactif (false)

Dans notre exemple le timer s'appelle timer1 et timer1.Interval est mis à 1000 ms (1s). L'événement Tick se produira donc toutes les secondes. Le clic sur le bouton Arrêt/Marche est traité par la procédure buttonArretMarche_Click suivante :

Cacher Sélectionnez
  1. using System;
  2. using System.Windows.Forms;
  3.
  4. namespace Chap5 {
  5.     public partial class Form1 : Form {
  6.         public Form1() {
  7.             InitializeComponent();
  8.         }
  9.
  10.         // variable d'instance
  11.         private DateTime début = DateTime.Now;
  12. ...
  13.         private void buttonArretMarche_Click(object sender, EventArgs e) {
  14.             // arrêt ou marche ?
  15.             if (buttonArretMarche.Text == "Marche") {
  16.                 // on note l'heure de début
  17.                 début = DateTime.Now;
  18.                 // on l'affiche
  19.                 labelChrono.Text = "00:00:00";
  20.                 // on lance le timer
  21.                 timer1.Enabled = true;
  22.                 // on change le libellé du bouton
  23.                 buttonArretMarche.Text = "Arrêt";
  24.                 // fin
  25.                 return;
  26.             }//
  27.             if (buttonArretMarche.Text == "Arrêt") {
  28.                 // arrêt du timer
  29.                 timer1.Enabled = false;
  30.                 // on change le libellé du bouton
  31.                 buttonArretMarche.Text = "Marche";
  32.                 // fin
  33.                 return;
  34.             }
  35.         }
  36.
  37.     }
  38. }
  • ligne 13 : la procédure qui traite le clic sur le bouton Arrêt/Marche.
  • ligne 15 : le libellé du bouton Arrêt/Marche est soit "Arrêt" soit "Marche". On est donc obligé de faire un test sur ce libellé pour savoir quoi faire.
  • ligne 17 : dans le cas de "Marche", on note l'heure de début dans une variable début qui est une variable globale (ligne 11) de l'objet formulaire
  • ligne 19 : initialise le contenu du label LabelChrono
  • ligne 21 : le timer est lancé (Enabled=true)
  • ligne 23 : libellé du bouton passe à "Arrêt".
  • ligne 27 : dans le cas de "Arrêt"
  • ligne 29 : on arrête le timer (Enabled=false)
  • ligne 31 : on passe le libellé du bouton à "Marche".

Il nous reste à traiter l'événement Tick sur l'objet timer1 , événement qui se produit toutes les secondes :

Cacher Sélectionnez
  1. private void timer1_Tick(object sender, EventArgs e) {
  2.             // une seconde s'est écoulée
  3.             DateTime maintenant = DateTime.Now;
  4.             TimeSpan durée = maintenant - début;
  5.             // on met à jour le chronomètre
  6.             labelChrono.Text = durée.Hours.ToString("d2") + ":" + durée.Minutes.ToString("d2") + ":" + durée.Seconds.ToString("d2");
  7.         }
  • ligne 3 : on note l'heure du moment
  • ligne 4 : on calcule le temps écoulé depuis l'heure de lancement du chronomètre. On obtient un objet de type TimeSpan qui représente une durée dans le temps.
  • ligne 6 : celle-ci doit être affichée dans le chronomètre sous la forme hh:mm:ss . Pour cela nous utilisons les propriétés Hours , Minutes , Seconds de l'objet TimeSPan qui représentent respectivement les heures, minutes, secondes de la durée que nous affichons au format ToString("d2") pour avoir un affichage sur 2 chiffres.

VIII-F. Application exemple - version 6

On reprend l'application exemple IMPOTS. La dernière version a été étudiée au paragraphe , page . C'était l'application à trois couches suivante :

Image non disponible
  • les couches [metier] et [dao] étaient encapsulées dans des DLL
  • la couche [ui] était une couche [console]
  • l'instanciation des couches et leur intégration dans l'application étaient assurées par Spring.

Dans cette nouvelle version, la couche [ui] sera assurée par l'interface graphique suivante :

Image non disponible

VIII-F-1. La solution Visual Studio

La solution Visual Studio est composée des éléments suivants :

Image non disponible
  • [1] : le projet est constitué des éléments suivants :
  • [Program.cs] : la classe qui lance l'application
  • [Form1.cs] : la classe d'un 1er formulaire
  • [Form2] : la classe d'un 2ième formulaire
  • [lib] détaillé dans [2] : on y a mis toutes les DLL nécessaires au projet :
  • [ImpotsV5-dao.dll] : la DLL de la couche [dao] générée au paragraphe , page
  • [ImpotsV5-metier.dll] : la DLL de la couche [dao] générée au paragraphe , page
  • [Spring.Core.dll], [Common.Logging.dll], [antlr.runtime.dll] : les DLL de Spring déjà utilisées dans la version précédente (cf paragraphe , page ).
  • [references] détaillé dans [3] : les références du projet. On a ajouté une référence pour chacune des DLL du dossier [lib]
  • [App.config] : le fichier de configuration du projet. Il est identique à celui de la version précédente décrit au paragraphe , page .
  • [DataImpot.txt] : le fichier des tranches d'impôt configuré pour être recopié automatiquement dans le dossier d'exécution du projet [4]

Le formulaire [Form1] est le formulaire de saisies des paramètres du calcul de l'impôt [A] déjà présenté plus haut. Le formulaire [Form2] [B] sert à afficher un message d'erreur :

Image non disponible

VIII-F-2. La classe [Program.cs]

La classe [Program.cs] lance l'application. Son code est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Windows.Forms;
  3. using Spring.Context;
  4. using Spring.Context.Support;
  5. using Metier;
  6. using System.Text;
  7.
  8. namespace Chap5 {
  9.     static class Program {
  10.         /// <summary>
  11.         /// The main entry point for the application.
  12.         /// </summary>
  13.         [STAThread]
  14.         static void Main() {
  15.             // code généré par Vs
  16.             Application.EnableVisualStyles();
  17.             Application.SetCompatibleTextRenderingDefault(false);
  18.
  19.             // --------------- Code développeur
  20.             // instanciations couches [metier] et [dao]
  21.             IApplicationContext ctx = null;
  22.             Exception ex = null;
  23.             IImpotMetier metier = null;
  24.             try {
  25.                 // contexte Spring
  26.                 ctx = ContextRegistry.GetContext();
  27.                 // on demande une référence sur la couche [metier]
  28.                 metier = (IImpotMetier)ctx.GetObject("metier");
  29.             } catch (Exception e1) {
  30.                 // mémorisation exception
  31.                 ex = e1;
  32.             }
  33.             // formulaire à afficher
  34.             Form form = null;
  35.             // y-a-t-il eu une exception ?
  36.             if (ex != null) {
  37.                 // oui - on crée le message d'erreur à afficher
  38.                 StringBuilder msgErreur = new StringBuilder(String.Format("Chaîne des exceptions : {0}{1}", "".PadLeft(40, '-'), Environment.NewLine));
  39.                 Exception e = ex;
  40.                 while (e != null) {
  41.                     msgErreur.Append(String.Format("{0}: {1}{2}", e.GetType().FullName, e.Message, Environment.NewLine));
  42.                     msgErreur.Append(String.Format("{0}{1}", "".PadLeft(40, '-'), Environment.NewLine));
  43.                     e = e.InnerException;
  44.                 }
  45.                 // création fenêtre d'erreur à laquelle on passe le message d'erreur à afficher
  46.                 Form2 form2 = new Form2();
  47.                 form2.MsgErreur = msgErreur.ToString();
  48.                 // ce sera la fenêtre à afficher
  49.                 form = form2;
  50.             } else {
  51.                 // tout s'est bien passé
  52.                 // création interface graphique [Form1] à laquelle on passe la référence sur la couche [metier]
  53.                 Form1 form1 = new Form1();
  54.                 form1.Metier = metier;
  55.                 // ce sera la fenêtre à afficher
  56.                 form = form1;
  57.             }
  58.             // affichage fenêtre
  59.             Application.Run(form);
  60.         }
  61.     }
  62. }

Le code généré par Visual Studio a été complété à partir de la ligne 19. L'application exploite le fichier [] suivant :

Cacher Sélectionnez
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.
  4.     <configSections>
  5.         <sectionGroup name="spring">
  6.             <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
  7.             <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
  8.         </sectionGroup>
  9.     </configSections>
  10.
  11.     <spring>
  12.         <context>
  13.             <resource uri="config://spring/objects" />
  14.         </context>
  15.         <objects xmlns="http://www.springframework.net">
  16.             <object name="dao" type="Dao.FileImpot, ImpotsV5-dao">
  17.                 <constructor-arg index="0" value="DataImpot.txt"/>
  18.             </object>
  19.             <object name="metier" type="Metier.ImpotMetier, ImpotsV5-metier">
  20.                 <constructor-arg index="0" ref="dao"/>
  21.             </object>
  22.         </objects>
  23.     </spring>
  24. </configuration>
  • lignes 24-32 : exploitation du fichier [App.config] précédent pour instancier les couches [metier] et [dao]
  • ligne 26 : exploitation du fichier [App.config]
  • ligne 28 : récupération d'une référence sur la couche [metier]
  • ligne 31 : mémorisation de l'éventuelle exception
  • ligne 34 : la référence form désignera le formulaire à afficher ( form1 ou form2 )
  • lignes 36-50 : s'il y a eu exception, on se prépare à afficher un formulaire de type [Form2]
  • lignes 38-44 : on construit le message d'erreur à afficher. Il est constitué de la concaténation des messages d'erreurs des différentes exceptions présentes dans la chaîne des exceptions.
  • ligne 46 : un formulaire de type [Form2] est créé.
  • ligne 47 : nous le verrons ultérieurement, ce formulaire a une propriété publique MsgErreur qui est le message d'erreur à afficher :
Cacher Sélectionnez
  1.         public string MsgErreur { private get; set; }

On renseigne cette propriété.

  • ligne 49 : la référence form qui désigne la fenêtre à afficher est initialisée. On notera le polymorphisme à l'œuvre. form2 n'est pas de type [Form] mais de type [Form2], un type dérivé de [Form].
  • lignes 50-57 : il n'y a pas eu d'exception. On se prépare à afficher un formulaire de type [Form1].
  • ligne 53 : un formulaire de type [Form1] est créé.
Cacher Sélectionnez
  1.                 public IImpotMetier Metier { private get; set; }

On renseigne cette propriété.

  • ligne 56 : la référence form qui désigne la fenêtre à afficher est initialisée. On notera de nouveau le polymorphisme à l'œuvre. form1 n'est pas de type [Form] mais de type [Form1], un type dérivé de [Form].
  • ligne 59 : la fenêtre référencée par form est affichée.

VIII-F-3. Le formulaire [Form1]

En mode [conception] le formulaire [Form1] est le suivant :

Image non disponible

Les contrôles sont les suivants

n° | type | nom | rôle
0 | GroupBox | groupBox1 | Text =Etes-vous marié(e)
1 | RadioButton | radioButtonOui | coché si marié
2 | RadioButton | radioButtonNon | coché si pas marié Checked =True
3 | NumericUpDown | numericUpDownEnfants | nombre d'enfants du contribuable Minimum =0, Maximum =20, Increment =1
4 | TextBox | textSalaire | salaire annuel du contribuable en euros
5 | Label | labelImpot | montant de l'impôt à payer BorderStyle =Fixed3D
6 | Button | buttonCalculer | lance le calcul de l'impôt
7 | Button | buttonEffacer | remet le formulaire dans l'état qu'il avait lors du chargement
8 | Button | buttonQuitter | pour quitter l'application

Règles de fonctionnement du formulaire

  • le bouton Calculer reste éteint tant qu'il n'y a rien dans le champ du salaire
  • si lorsque le calcul est lancé, il s'avère que le salaire est incorrect, l'erreur est signalée [9]

Le code de la classe est le suivant :

Cacher Sélectionnez
  1. using System.Windows.Forms;
  2. using Metier;
  3. using System;
  4.
  5. namespace Chap5 {
  6.     public partial class Form1 : Form {
  7.         // couche [métier]
  8.         public IImpotMetier Metier { private get; set; }
  9.
  10.         public Form1() {
  11.             InitializeComponent();
  12.         }
  13.
  14.         private void buttonCalculer_Click(object sender, System.EventArgs e) {
  15.             // le salaire est-il correct
  16.             int salaire;
  17.             bool ok=int.TryParse(textSalaire.Text.Trim(), out salaire);
  18.             if (! ok  || salaire < 0) {
  19.                 // msg d'erreur
  20.                 MessageBox.Show("Salaire incorrect", "Erreur de saisie", MessageBoxButtons.OK, MessageBoxIcon.Error);
  21.                 // retour sur le champ erroné
  22.                 textSalaire.Focus();
  23.                 // sélection du texte du champ de saisie
  24.                 textSalaire.SelectAll();
  25.                 // retour à l'interface de saisie
  26.                 return;
  27.             }
  28.             // le salaire est correct - on peut calculer l'impôt
  29.             labelImpot.Text = Metier.CalculerImpot(radioButtonOui.Checked, (int)numericUpDownEnfants.Value, salaire).ToString();
  30.         }
  31.
  32.         private void buttonQuitter_Click(object sender, System.EventArgs e) {
  33.             Environment.Exit(0);
  34.         }
  35.
  36.         private void buttonEffacer_Click(object sender, System.EventArgs e) {
  37.             // raz formulaire
  38.             labelImpot.Text = "";
  39.             numericUpDownEnfants.Value = 0;
  40.             textSalaire.Text = "";
  41.             radioButtonNon.Checked = true;
  42.         }
  43.
  44.         private void textSalaire_TextChanged(object sender, EventArgs e) {
  45.             // état bouton [Calculer]
  46.             buttonCalculer.Enabled=textSalaire.Text.Trim()!="";
  47.         }
  48.
  49.     }
  50. }

Nous ne commentons que les parties importantes :

  • ligne [8] : la propriété publique Metier qui permet à la classe de lancement [Program.cs] d'injecter dans [Form1] une référence sur la couche [metier].
  • ligne [14] : la procédure de calcul de l'impôt
  • lignes 15-27 : vérification de la validité du salaire (un nombre entier >=0).
  • ligne 29 : calcul de l'impôt à l'aide de la méthode [CalculerImpot] de la couche [metier]. On notera la simplicité de cette opération obtenue grâce à l'encapsulation de la couche [metier] dans une DLL.

VIII-G. Le formulaire [Form2]

En mode [conception] le formulaire [Form2] est le suivant :

Image non disponible

Les contrôles sont les suivants

n° | type | nom | rôle
1 | TextBox | textBoxErreur | Multiline =True, Scrollbars =Both

Le code de la classe est le suivant :

Cacher Sélectionnez
  1. using System.Windows.Forms;
  2.
  3. namespace Chap5 {
  4.     public partial class Form2 : Form {
  5.         // msg d'erreur
  6.         public string MsgErreur { private get; set; }
  7.
  8.         public Form2() {
  9.             InitializeComponent();
  10.         }
  11.
  12.         private void Form2_Load(object sender, System.EventArgs e) {
  13.             // on affiche le msg d'erreur
  14.             textBoxErreur.Text = MsgErreur;
  15.             // on désélectionne tout le texte
  16.             textBoxErreur.Select(0, 0);
  17.         }
  18.     }
  19. }
  • ligne 6 : la propriété publique MsgErreur qui permet à la classe de lancement [Program.cs] d'injecter dans [Form2] le message d'erreur à afficher. Ce message est affiché lors du traitement de l'événement Load , lignes 12-16.
  • ligne 14 : le message d'erreur est mis dans le TextBox
  • ligne 16 : on enlève la sélection qui a lieu lors de l'opération précédente. [TextBox].Select(début,longueur) sélectionne (mise en surbrillance) longueur caractères à partir du caractère n° début . [TextBox].Select(0,0) revient à désélectionner tout texte.

VIII-G-1. Conclusion

Revenons sur l'architecture trois couches utilisée :

Image non disponible

Cette architecture nous a permis de substituer une implémentation graphique à l'implémentation console de la couche [ui] existante, sans rien changer aux couches [metier] et [dao]. Nous avons pu nous concentrer sur la couche [ui] sans nous inquiéter des impacts possibles sur les autres couches. C'est là le principal intérêt des architectures 3 couches. Nous en verrons un autre exemple plus loin, lorsque la couche [dao] qui exploite actuellement les données d'un fichier texte, sera remplacée par une couche [dao] exploitant les données d'une base de données. Nous verrons que cela se fera sans impact sur les couches [ui] et [metier].


CSharp tutoriel 2008

Apprentissage du langage C#

  • IX. Evénements utilisateur
    • IX-A. Objets delegate prédéfinis
    • IX-B. Définir des objets delegate
    • IX-C. Delegates ou interfaces ?
    • IX-D. Gestion d'événements

IX. Evénements utilisateur

Nous avons dans le chapitre précédent abordé la notion d'événements liés à des composants de formulaire. Nous voyons maintenant comment créer des événements dans nos propres classes.

IX-A. Objets delegate prédéfinis

La notion d'objet delegate a été rencontrée dans le chapitre précédent mais elle avait été alors survolée. Lorsque nous avions regardé comment les gestionnaires des événements des composants d'un formulaire étaient déclarés, nous avions rencontré du code similaire au suivant :

Cacher Sélectionnez
  1.             this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);

où buttonAfficher était un composant de type [Button]. Cette classe a un champ Click défini comme suit :

Image non disponible
  • [1] : la classe [Button]
  • [2] : ses événements
  • [3,4] : l'événement Click

Le delegate EventHandler est défini comme suit :

Image non disponible

Le delegate EventHandler désigne un modèle de méthode :

  • ayant pour 1er paramètre un type Object
  • ayant pour 2ième paramètre un type EventArgs
  • ne rendant aucun résultat

Une méthode correspondant au modèle défini par EventHandler pourrait être la suivante :

Cacher Sélectionnez
  1.         private void buttonAfficher_Click(object sender, EventArgs e);

Pour créer un objet de type EventHandler , on procède comme suit :

Cacher Sélectionnez
  1. EventHandler evtHandler=new EventHandler(méthode correspondant au prototype  défini par le type EventHandler);

On pourra ainsi écrire :

Cacher Sélectionnez
  1. EventHandler evtHandler=new EventHandler(buttonAfficher_Click);

Une variable de type delegate est en fait une liste de références sur des méthodes correspondant au modèle du delegate . Pour ajouter une nouvelle méthode M à la variable evtHandler ci-dessus, on utilise la syntaxe :

Cacher Sélectionnez
  1. evtHandler+=new EvtHandler(M);

La notation += peut être utilisée même si evtHandler est une liste vide.

L'instruction :

Cacher Sélectionnez
  1.             this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);

ajoute une méthode de type EventHandler à la liste des méthodes de l'événement buttonAfficher.Click . Lorsque l'événement Click sur le composant buttonAfficher se produit, VB exécute l'instruction :

Cacher Sélectionnez
  1.             buttonAfficher.Click(source, evt);

où :

  • source est le composant de type object à l'origine de l'événement
  • evt de type EventArgs et ne contient pas d'information

Toutes les méthodes de signature void M(object,EventArgs) qui ont été associées à l'événement Click par :

Cacher Sélectionnez
  1.             this.buttonAfficher.Click += new System.EventHandler(M);

seront appelées avec les paramètres ( source , evt ) transmis par VB.

IX-B. Définir des objets delegate ▲

L'instruction

Cacher Sélectionnez
  1.     public delegate int Opération(int n1, int n2);

définit un type appelé O pération qui représente un prototype de fonction acceptant deux entiers et rendant un entier. C'est le mot clé delegate qui fait de Opération une définition de prototype de fonction.

Une variable op de type Opération aura pour rôle d'enregistrer une liste de fonctions correspondant au prototype Opération :

int f1(int,int)
int f2(int,int)
…
int fn(int,int)

L'enregistrement d'une méthode fi dans la variable op se fait par op=new Opération(fi) ou plus simplement par op=fi. Pour ajouter une méthode fj à la liste des fonctions déjà enregistrées, on écrit op+= fj . Pour enlever une méthode fk déjà enregistrée on écrit op-=fk . Si dans notre exemple on écrit n=op(n1,n2) , l'ensemble des méthodes enregistrées dans la variable op seront exécutées avec les paramètres n1 et n2 . Le résultat n récupéré sera celui de la dernière méthode exécutée. Il n'est pas possible d'obtenir les résultats produits par l'ensemble des méthodes. Pour cette raison, si on enregistre une liste de méthodes dans une fonction déléguée, celles-ci rendent le plus souvent un résultat de type void .

Considérons l'exemple suivant :

Cacher Sélectionnez
  1. using System;
  2. namespace Chap6 {
  3.     class Class1 {
  4.         // définition d'un prototype de fonction
  5.         // accepte 2 entiers en paramètre et rend un entier
  6.         public delegate int Opération(int n1, int n2);
  7.
  8.         // deux méthodes d'instance correspondant au prototype
  9.         public int Ajouter(int n1, int n2) {
  10.             Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
  11.             return n1 + n2;
  12.         }//ajouter
  13.
  14.         public int Soustraire(int n1, int n2) {
  15.             Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
  16.             return n1 - n2;
  17.         }//soustraire
  18.
  19.         // une méthode statique correspondant au prototype
  20.         public static int Augmenter(int n1, int n2) {
  21.             Console.WriteLine("Augmenter(" + n1 + "," + n2 + ")");
  22.             return n1 + 2 * n2;
  23.         }//augmenter
  24.
  25.         static void Main(string[] args) {
  26.
  27.             // on définit un objet de type opération pour y enregistrer des fonctions
  28.             // on enregistre la fonction statique augmenter
  29.             Opération op = Augmenter;
  30.             // on exécute le délégué
  31.             int n = op(4, 7);
  32.             Console.WriteLine("n=" + n);
  33.
  34.             // création d'un objet c1 de type class1
  35.             Class1 c1 = new Class1();
  36.             // on enregistre dans le délégué la méthode ajouter de c1
  37.             op = c1.Ajouter;
  38.             // exécution de l'objet délégué
  39.             n = op(2, 3);
  40.             Console.WriteLine("n=" + n);
  41.             // on enregistre dans le délégué la méthode soustraire de c1
  42.             op = c1.Soustraire;
  43.             n = op(2, 3);
  44.             Console.WriteLine("n=" + n);
  45.             //enregistrement de deux fonctions dans le délégué
  46.             op = c1.Ajouter;
  47.             op += c1.Soustraire;
  48.             // exécution de l'objet délégué
  49.             op(0, 0);
  50.             // on retire une fonction du délégué
  51.             op -= c1.Soustraire;
  52.             // on exécute le délégué
  53.             op(1, 1);
  54.         }
  55.     }
  56. }
  • ligne 3 : définit une classe Class1 .
  • ligne 6 : définition du delegate Opération : un prototype de méthodes acceptant deux paramètres de type int et rendant un résultat de type int
  • lignes 9-12 : la méthode d'instance Ajouter a la signature du delegate Opération .
  • lignes 14-17 : la méthode d'instance S oustraire a la signature du delegate Opération .
  • lignes 20-23 : la méthode de classe A ugmenter a la signature du delegate Opération .
  • ligne 25 : la méthode Main exécutée
  • ligne 20 : la variable op est de type delegate Opération . Elle contiendra une liste de méthodes ayant la signature du type delegate Opération. On lui affecte une première référence de méthode, celle sur la méthode statique Class1.Augmenter .
  • ligne 31 : le delegate op est exécuté : ce sont toutes les méthodes référencées par op qui vont être exécutées. Elles le seront avec les paramètres passés au delegate op. Ici, seule la méthode statique Class1.Augmenter va être exécutée.
  • ligne 35 : une instance c1 de la classe Class1 est créée.
  • ligne 37 : la méthode d'instance c1.Ajouter est affectée au delegate op . Augmenter était une méthode statique, Ajouter est une méthode d'instance. On a voulu montrer que cela n'avait pas d'importance.
  • ligne 39 : le delegate op est exécuté : la méthode Ajouter va être exécutée avec les paramètres passés au delegate op .
  • ligne 42 : on refait de même avec la méthode d'instance Soustraire .
  • lignes 46-47 : on met les méthodes Ajouter et Soustraire dans le delegate op .
  • ligne 49 : le delegate op est exécuté : les deux méthodes Ajouter et Soustraire vont être exécutées avec les paramètres passés au delegate op .
  • ligne 51 : la méthode Soustraire est enlevée du delegate op .
  • ligne 53 : le delegate op est exécuté : la méthode restante Ajouter va être exécutée.

Les résultats de l'exécution sont les suivants :

Cacher Sélectionnez
  1. Augmenter(4,7)
  2. n=18
  3. Ajouter(2,3)
  4. n=5
  5. Soustraire(2,3)
  6. n=-1
  7. Ajouter(0,0)
  8. Soustraire(0,0)
  9. Ajouter(1,1)

IX-C. Delegates ou interfaces ? ▲

Les notions de delegates et d'interfaces peuvent sembler assez proches et on peut se demander quelles sont exactement les différences entre ces deux notions. Prenons l'exemple suivant proche d'un exemple déjà étudié :

Cacher Sélectionnez
  1. using System;
  2. namespace Chap6 {
  3.     class Program1 {
  4.         // définition d'un prototype de fonction
  5.         // accepte 2 entiers en paramètre et rend un entier
  6.         public delegate int Opération(int n1, int n2);
  7.
  8.         // deux méthodes d'instance correspondant au prototype
  9.         public static int Ajouter(int n1, int n2) {
  10.             Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
  11.             return n1 + n2;
  12.         }//ajouter
  13.
  14.         public static int Soustraire(int n1, int n2) {
  15.             Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
  16.             return n1 - n2;
  17.         }//soustraire
  18.
  19.         // Exécution d'un délégué
  20.         public static int Execute(Opération op, int n1, int n2){
  21.             return op(n1, n2);
  22.         }
  23.
  24.         static void Main(string[] args) {
  25.             // exécution du délégué Ajouter
  26.             Console.WriteLine(Execute(Ajouter, 2, 3));
  27.             // exécution du délégué Soustraire
  28.             Console.WriteLine(Execute(Soustraire, 2, 3));
  29.             // exécution d'un délégué multicast
  30.             Opération op = Ajouter;
  31.             op += Soustraire;
  32.             Console.WriteLine(Execute(op, 2, 3));
  33.             // on retire une fonction du délégué
  34.             op -= Soustraire;
  35.             // on exécute le délégué
  36.             Console.WriteLine(Execute(op, 2, 3));
  37.         }
  38.     }
  39. }

Ligne 20, la méthode Execute attend une référence sur un objet du type delegate Opération défini ligne 6. Cela permet de passer à la méthode Execute , différentes méthodes (lignes 26, 28, 32 et 36). Cette propriété de polymorphisme peut être obtenue également avec une interface :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap6 {
  4.
  5.     // interface IOperation
  6.     public interface IOperation {
  7.         int operation(int n1, int n2);
  8.     }
  9.
  10.     // classe Ajouter
  11.     public class Ajouter : IOperation {
  12.         public int operation(int n1, int n2) {
  13.             Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
  14.             return n1 + n2;
  15.         }
  16.     }
  17.
  18.     // classe Soustraire
  19.     public class Soustraire : IOperation {
  20.         public int operation(int n1, int n2) {
  21.             Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
  22.             return n1 - n2;
  23.         }
  24.     }
  25.
  26.     // classe de test
  27.     public static class Program2 {
  28.         // Exécution de la méthode unique de l'interface IOperation
  29.         public static int Execute(IOperation op, int n1, int n2) {
  30.             return op.operation(n1, n2);
  31.         }
  32.
  33.         public static void Main() {
  34.             // exécution du délégué Ajouter
  35.             Console.WriteLine(Execute(new Ajouter(), 2, 3));
  36.             // exécution du délégué Soustraire
  37.             Console.WriteLine(Execute(new Soustraire(), 2, 3));
  38.         }
  39.     }
  40. }
  • lignes 6-8 : l'interface [IOperation] définit une méthode operation .
  • lignes 11-16 et 19-24 : les classes [Ajouter] et [Soustraire] implémentent l'interface [IOperation].
  • lignes 29-31 : la méthode Execute dont le 1er paramètre est du type de l'interface IOperation . La méthode Execute va recevoir successivement comme 1er paramètre, une instance de la classe Ajouter puis une instance de la classe Soustraire .

On retrouve bien l'aspect polymorphique qu'avait le paramètre de type delegate de l'exemple précédent. Les deux exemples montrent en même temps des différences entre ces deux notions.

Les types delegate et interface sont interchangeables

  • si l'interface n'a qu'une méthode. En effet, le type delegate est une enveloppe pour une unique méthode alors que l'interface peut, elle, définir plusieurs méthodes.
  • si l'aspect multicast du delegate n'est pas utilisé. Cette notion de multicast n'existe en effet pas dans l'interface.

Si ces deux conditions sont vérifiées, alors on a le choix entre les deux signatures suivantes pour la méthode Execute :

Cacher Sélectionnez
  1. int Execute(IOperation op, int n1, int n2)
  2. int Execute(Opération op, int n1, int n2)

La seconde qui utilise le delegate peut se montrer plus souple d'utilisation. En effet dans la première signature, le premier paramètre de la méthode doit implémenter l'interface IOperation . Cela oblige à créer une classe pour y définir la méthode appelée à être passée en premier paramètre à la méthode Execute . Dans la seconde signature, toute méthode existante ayant la bonne signature fait l'affaire. Il n'y a pas de construction supplémentaire à faire.

IX-D. Gestion d'événements ▲

Les objets delegate peuvent servir à définir des événements. Une classe C1 peut définir un événement evt de la façon suivante :

  • un type delegate est défini dans ou en-dehors de la classe C1 :
Cacher Sélectionnez
  1. delegate TResult Evt(T1 param1, T2 param2, ...);
  • la classe C1 définit un champ de type delegate Evt :
Cacher Sélectionnez
  1. public Evt Evt1;
  • lorsque une instance c1 de la classe C1 voudra signaler un événement, elle exécutera son delegate Evt1 en lui passant les paramètres définis par le delegate Evt . Toutes les méthodes enregistrées dans le delegate Evt1 seront alors exécutées avec ces paramètres. On peut dire qu'elles ont été averties de l'événement Evt1 .
  • si un objet c 2 utilisant un objet c 1 veut être averti de l'occurrence de l'événement Evt1 sur l'objet c 1 , il enregistrera l'une de ses méthodes c 2.M dans l'objet délégué c 1.Evt1 de l'objet c 1. Ainsi sa méthode c 2.M sera exécutée à chaque fois que l'événement E vt1 se produira sur l'objet c 1 . Il pourra également de désinscrire lorsqu'il ne voudra plus être averti de l'événement.
  • comme l'objet délégué c 1.Evt1 peut enregistrer plusieurs méthodes, différents objets c i pourront s'enregistrer auprès du délégué c 1.Evt1 pour être prévenus de l'événement Evt1 sur c 1 .

Dans ce scénario, on a :

  • une classe qui signale un événement
  • des classes qui sont averties de cet événement. On dit qu'elles souscrivent à l'événement ou s'abonnent à l'événement.
  • un type delegate qui définit la signature des méthodes qui seront averties de l'événement

Le framework .NET définit :

  • une signature standard du delegate d'un événement
Cacher Sélectionnez
  1. public delegate void MyEventHandler(object source, EventArgs evtInfo);
  • source : l'objet qui a signalé l'événement
  • evtInfo : un objet de type EventArgs ou dérivé qui apporte des informations sur l'événement
  • le nom du delegate doit être terminé par EventHandler
  • une façon standard pour déclarer un événement de type MyEventHandler dans une classe :
Cacher Sélectionnez
  1. public Class C1{
  2.     public event MyEventHandler Evt1;
  3. ...
  4. }

Le champ E vt1 est de type delegate . Le mot clé event est là pour restreindre les opérations qu'on peut faire sur lui :

  • de l'extérieur de la classe C1 , seules les opérations += et -= sont possibles. Cela empêche la suppression (par erreur du développeur par exemple) des méthodes abonnées à l'événement. On peut simplement s'abonner (+=) ou se désabonner (-=) de l'événement.
  • seule une instance de type C1 peut exécuter l'appel Evt1(source,evtInfo) qui déclenche l'exécution des méthodes abonnées à l'événement Evt1.

Le framework .NET fournit une méthode générique satisfaisant à la signature du delegate d'un événement :

Cacher Sélectionnez
  1. public delegate void EventHandler<TEventArgs>(object source, TEventArgs evtInfo) where TEventArgs : EventArgs
  • le delegate EventHandler utilise le type générique TEventArgs qui est le type de son 2ième paramètre
  • le type TEventArgs doit dériver du type EventsArgs ( where TEventArgs : EventArgs )

Avec ce delegate générique, la déclaration d'un événement X dans la classe C suivra le schéma conseillé suivant :

  • définir un type XEventArgs dérivé de EventArgs pour encapsuler les informations sur l'événement X
  • définir dans la classe C un événement de type EventHandler<XEventArgs> .
  • définir dans la classe C une méthode protégée
Cacher Sélectionnez
  1. protected void OnXHandler(XEventArgs e);

destinée à "publier" l'événement X aux abonnés.

Considérons l'exemple suivant :

  • une classe Emetteur encapsule une température. Cette température est observée. Lorsque cette température dépasse un certain seuil, un événement doit être lancé. Nous appellerons cet événement TemperatureTropHaute . Les informations sur cet événement seront encapsulées dans un type TemperatureTropHauteEventArgs.
  • une classe Souscripteur s'abonne à l'événement précédent. Lorsqu'elle est avertie de l'événement, elle affiche un message sur la console.
  • un programme console crée un émetteur et deux abonnés. Il saisit les températures au clavier et les enregistre dans une instance Emetteur . Si celle-ci est trop haute, l'instance Emetteur publie l'événement TemperatureTropHaute.

Pour se conformer à la méthode conseillée de gestion des événements, nous définissons tout d'abord le type TemperatureTropHauteEventArgs pour encapsuler les informations sur l'événement :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap6 {
  4.     public class TemperatureTropHauteEventArgs:EventArgs {
  5.         // température lors de l'évt
  6.         public decimal Temperature { get; set; }
  7.         // constructeurs
  8.         public TemperatureTropHauteEventArgs() {
  9.         }
  10.         public TemperatureTropHauteEventArgs(decimal temperature) {
  11.             Temperature = temperature;
  12.         }
  13.     }
  14. }
  • ligne 6 : l'information encapsulée par la classe TemperatureTropHauteEventArgs est la température qui a provoqué l'événement TemperatureTropHaute .

La classe Emetteur est la suivante :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap6 {
  4.     public class Emetteur {
  5.         static decimal SEUIL = 19;
  6.
  7.         // température observée
  8.         private decimal temperature;
  9.         // nom de la source
  10.         public string Nom { get; set; }
  11.         // évt signalé
  12.         public event EventHandler<TemperatureTropHauteEventArgs> TemperatureTropHaute;
  13.
  14.         // lecture / écriture température
  15.         public decimal Temperature {
  16.             get {
  17.                 return temperature;
  18.             }
  19.             set {
  20.                 temperature = value;
  21.                 if (temperature > SEUIL) {
  22.                     // on signale l'événement aux abonnés
  23.                     OnTemperatureTropHaute(new TemperatureTropHauteEventArgs(temperature));
  24.                 }
  25.             }
  26.         }
  27.
  28.         // signalement d'un évt
  29.         protected virtual void OnTemperatureTropHaute(TemperatureTropHauteEventArgs evt) {
  30.             // émission de l'évt TemperatureTropHaute aux abonnés
  31.             TemperatureTropHaute(this, evt);
  32.         }
  33.     }
  34. }
  • ligne 5 : le seuil de température au-delà duquel l'événement TemperatureTropHaute sera publié.
  • ligne 10 : l'émetteur a un nom pour être identifié
  • ligne 12 : l'événement TemperatureTropHaute .
  • lignes 15-26 : la méthode get qui rend la température et la méthode set qui l'enregistre. C'est la méthode set qui fait publier l'événement TemperatureTropHaute si la température à enregistrer dépasse le seuil de la ligne 5. Elle fait publier l'événement par la méthode OnTemperatureTropHauteHandler de la ligne 29 en lui passant pour paramètre un objet TemperatureTropHauteEventArgs dans lequel on a enregistré la température qui a dépassé le seuil.
  • lignes 29-32 : l'événement TemperatureTropHaute est publié avec pour 1er paramètre l'émetteur lui-même et pour second paramètre l'objet TemperatureTropHauteEventArgs reçu en paramètre.

La classe Souscripteur qui va s'abonner à l'événement TemperatureTropHaute est la suivante :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace Chap6 {
  4.     public class Souscripteur {
  5.         // nom
  6.         public string Nom { get; set; }
  7.
  8.         // gestionnaire de l'évt TemperatureTropHaute
  9.         public void EvtTemperatureTropHaute(object source, TemperatureTropHauteEventArgs e) {
  10.             // affichage console opérateur
  11.             Console.WriteLine("Souscripteur [{0}] : la source [{1}] a signalé une température trop haute : [{2}]", Nom, ((Emetteur)source).Nom, e.Temperature);
  12.         }
  13.     }
  14. }
  • ligne 6 : chaque souscripteur est identifié par un nom.
  • lignes 9-12 : la méthode qui sera associée à l'événement TemperatureTropHaute . Elle a la signature du type delegate EventHandler<TEventArgs> qu'un gestionnaire d'événement doit avoir. La méthode affiche sur la console : le nom du souscripteur qui affiche le message, le nom de l'émetteur qui a signalé l'événement, la température qui a déclenché ce dernier.
  • l'abonnement à l'événement TemperatureTropHaute d'un objet Emetteur n'est pas fait dans la classe Souscripteur . Il sera fait par une classe externe.

Le programme [Program.cs] lie tous ces éléments entre-eux :

Cacher Sélectionnez
  1. using System;
  2. namespace Chap6 {
  3.     class Program {
  4.         static void Main(string[] args) {
  5.             // création d'un émetteur d'evts
  6.             Emetteur e1 = new Emetteur() { Nom = "e" };
  7.             // création d'un tableau de 2 souscripteurs
  8.             Souscripteur[] souscripteurs = new Souscripteur[2];
  9.             for (int i = 0; i < souscripteurs.Length; i++) {
  10.                 // création souscripteur
  11.                 souscripteurs[i] = new Souscripteur() { Nom = "s" + i };
  12.                 // on l'abonne à l'évt TemperatureTropHaute de e1
  13.                 e1.TemperatureTropHaute += souscripteurs[i].EvtTemperatureTropHaute;
  14.             }
  15.             // on lit les températures au clavier
  16.             decimal temperature;
  17.             Console.Write("Température (rien pour arrêter) : ");
  18.             string saisie = Console.ReadLine().Trim();
  19.             // tant que la ligne saisie est non vide
  20.             while (saisie != "") {
  21.                 // la saisie est-elle un nombre décimal ?
  22.                 if (decimal.TryParse(saisie, out temperature)) {
  23.                     // température correcte - on l'enregistre
  24.                     e1.Temperature = temperature;
  25.                 } else {
  26.                     // on signale l'erreur
  27.                     Console.WriteLine("Température incorrecte");
  28.                 }
  29.                 // nouvelle saisie
  30.                 Console.Write("Température (rien pour arrêter) : ");
  31.                 saisie = Console.ReadLine().Trim();
  32.             }//while
  33.         }
  34.     }
  35. }
  • ligne 6 : création de l'émetteur
  • lignes 8-14 : création de deux souscripteurs qu'on abonne à l'événement TemperatureTropHaute de l'émetteur.
  • lignes 20-32 : boucle de saisie des températures au clavier
  • ligne 24 : si la température saisie est correcte, elle est transmise à l'objet Emetteur e1 qui déclenchera l'événement TemperatureTropHaute si la température est supérieure à 19 ° C.

Les résultats de l'exécution sont les suivants :

Cacher Sélectionnez
  1. Température (rien pour arrêter) : 17
  2. Température (rien pour arrêter) : 21
  3. Souscripteur [s0] : la source [e] a signalé une température trop haute : [21]
  4. Souscripteur [s1] : la source [e] a signalé une température trop haute : [21]


CSharp tutoriel 2008

Apprentissage du langage C#

  • X. Accès aux bases de données
    • X-A. Connecteur ADO.NET
    • X-B. Les deux modes d'exploitation d'une source de données
    • X-C. Les concepts de base de l'exploitation d'une base de données
      • X-C-1. La base de données exemple
      • X-C-2. Les quatre commandes de base du langage SQL
      • X-C-3. Les interfaces de base d'ADO.NET pour le mode connecté
      • X-C-4. La gestion des erreurs
      • X-C-5. Configuration du projet exemple
      • X-C-6. Le programme exemple
      • X-C-7. Exécution d'une requête SELECT
      • X-C-8. Exécution d'un ordre de mise à jour : INSERT, UPDATE, DELETE
    • X-D. Autres connecteurs ADO.NET
      • X-D-1. Connecteur SQL Server 2005
      • X-D-2. Connecteur MySQL5
      • X-D-3. Connecteur ODBC
      • X-D-4. Connecteur OLE DB
      • X-D-5. Connecteur générique
      • X-D-6. Quel connecteur choisir ?
    • X-E. Requêtes paramétrées
    • X-F. Transactions
      • X-F-1. Généralités
      • X-F-2. L'API de gestion des transactions
      • X-F-3. Le programme exemple
    • X-G. La méthode ExecuteScalar
    • X-H. Application exemple - version 7
      • X-H-1. La base de données
      • X-H-2. La solution Visual Studio
      • X-H-3. La couche [dao]
      • X-H-4. La couche [metier]
      • X-H-5. La couche [ui]
      • X-H-6. Changer la base de données
    • X-I. Pour aller plus loin …

X. Accès aux bases de données

X-A. Connecteur ADO.NET

Reprenons l'architecture en couches utilisée à diverses reprises

Image non disponible

Dans les exemples étudiés, la couche [dao] a pour l'instant exploité deux types de sources de données :

  • des données placées en dur dans le code
  • des données provenant de fichiers texte

Nous étudions dans ce chapitre le cas où les données proviennent d'une base de données. L'architecture 3 couches évolue alors vers une architecture multicouches. Il en existe diverses. Nous allons étudier les concepts de base avec la suivante :

Image non disponible

Dans le schéma ci-dessus, la couche [dao] [1] dialogue avec le SGBD [3] au travers d'une bibliothèque de classes propre au SGBD utilisé et livrée avec lui. Cette couche implémente des fonctionnalités standard réunies sous le vocable ADO (Active X Data Objects). On appelle une telle couche, un provider (fournisseur d'accès à une base de données ici) ou encore connecteur. La plupart des SGBD disposent désormais d'un connecteur ADO.NET, ce qui n'était pas le cas aux débuts de la plate-forme .NET. Les connecteurs .NET n'offrent pas une interface standard à la couche [dao], aussi celle-ci a-t-elle dans son code le nom des classes du connecteur. Si on change de SGBD, on change de connecteur et de classes et il faut alors changer la couche [dao]. C'est à la fois une architecture performante parce le connecteur .NET ayant été écrit pour un SGBD particulier sait utiliser au mieux celui-ci et rigide car changer de SGBD implique de changer la couche [dao]. Ce deuxième argument est à relativiser : les entreprises ne changent pas de SGBD très souvent. Par ailleurs, nous verrons ultérieurement que depuis la version 2.0 de .NET, il existe un connecteur générique qui amène de la souplesse sans sacrifier la performance.

X-B. Les deux modes d'exploitation d'une source de données

La plate-forme .NET permet l'exploitation d'une source de données de deux manières différentes :

  • mode connecté
  • mode déconnecté

En mode connecté, l'application

  • ouvre une connexion avec la source de données
  • travaille avec la source de données en lecture/écriture
  • ferme la connexion

En mode déconnecté, l'application

  • ouvre une connexion avec la source de données
  • obtient une copie mémoire de tout ou partie des données de la source
  • ferme la connexion
  • travaille avec la copie mémoire des données en lecture/écriture
  • lorsque le travail est fini, ouvre une connexion, envoie les données modifiées à la source de données pour qu'elle les prenne en compte, ferme la connexion

Nous n'étudions ici que le mode connecté.

X-C. Les concepts de base de l'exploitation d'une base de données

Nous allons exposer les principaux concepts d'utilisation d'une base de données avec une base de données SQL Server Compact 3.5. Ce SGBD est livré avec Visual Studio Express. C'est un SGBD léger qui ne sait gérer qu'un utilisateur à la fois. Il est cependant suffisant pour introduire la programmation avec les bases de données. Ultérieurement, nous présenterons d'autres SGBD.

L'architecture utilisée sera la suivante :

Image non disponible

Une application console [1] exploitera une base de données de type SqlServer Compact [3,4] via le connecteur Ado.Net de ce SGBD [2].

X-C-1. La base de données exemple

Nous allons construire la base de données directement dans Visual studio Express. Pour cela, nous créons un nouveau projet de type console.

Image non disponible
  • [1] : le projet
  • [2] : on ouvre une vue "Explorateur de bases de données"
  • [3] : on crée une nouvelle connexion
Image non disponible
  • [4] : on sélectionne le type du SGBD
  • [5,6] : on choisit le SGBD SQL Server Compact
  • [7] : on crée la base de données
  • [8] : une base de données SQL Server Compact est encapsulée dans un unique fichier de suffixe . sdf . On indique où la créer, ici dans le dossier du projet C#.
  • [9] : on a donné le nom [dbarticles.sdf] à la nouvelle base
  • [10] : on sélectionne la langue française. Cela a une conséquence sur les opérations de tri.
  • [11,12] : la base de données peut être protégée par un mot de passe. Ici " dbarticles ".
  • [13] : on valide la page de renseignements. La base va être physiquement créée :
Image non disponible
  • [14] : le nom de la base qui vient d'être créée
  • [15] : on coche l'option "Save my password" afin de ne pas avoir à le retaper à chaque fois
  • [16] : on vérifie la connexion
  • [17] : tout va bien
  • [18] : on valide la page d'informations
  • [19] : la connexion apparaît dans l'explorateur de bases de données
  • [20] : pour l'instant la base est sans tables. On en crée une. Un article aura les champs suivants :
  • id : un identifiant unique - clé primaire
  • nom : nom de l'article - unique
  • prix : prix de l'article
  • stockactuel : son stock actuel
  • stockminimum : le stock minimum en-deça duquel il faut réapprovisionner l'article
Image non disponible
  • [21] : le champ [id] est de type entier et est clé primaire [22] de la table.
  • [23] : cette clé primaire est de type Identity . Cette notion propre aux SGBD SQL Server indique que la clé primaire sera générée par le SGBD lui-même. Ici la clé primaire sera un nombre entier commençant à 1 et incrémenté de 1 à chaque nouvelle clé.
Image non disponible
  • [24] : les autres champs sont créés. On notera que le champ [nom] a une [25].
  • [26] : on donne un nom à la table
  • [27] : après avoir validé la structure de la table, celle-ci apparaît dans la base.
Image non disponible
  • [28] : on demande à voir le contenu de la table
  • [29] : elle est vide pour l'instant
  • [30] : on la remplit avec quelques données. Une ligne est validée dès qu'on passe à la saisie de la ligne suivante. Le champ [id] n'est pas saisi : il est généré automatiquement lorsque la ligne est validée.

Il nous reste à configurer le projet pour que cette base qui est actuellement à la racine du projet soit recopiée automatiquement dans le dossier d'exécution du projet :

Image non disponible
  • [1] : on demande à voir tous les fichiers
  • [2] : la base [dbarticles.sdf] apparaît
  • [3] : on l'inclut dans le projet Image non disponible
  • [4] : l'opération d'ajout d'une source de données dans un projet lance un assistant dont nous n'avons pas besoin ici [5].
  • [6] : la base fait maintenant partie du projet. On revient en mode normal [7].
  • [8] : le projet avec sa base
  • [9] : dans les propriétés de la base, on peut voir [10] que celle-ci sera automatiquement recopiée dans le dossier d'exécution du projet. C'est là que le programme que nous allons écrire, ira la chercher.

Maintenant que nous avons une base de données disponible, nous allons pouvoir l'exploiter. Auparavant, nous faisons quelques rappels SQL.

X-C-2. Les quatre commandes de base du langage SQL

SQL (Structured Language Query) est un langage, partiellement normalisé, d'interrogation et de mise à jour des bases de données. Tous les SGBD respectent la partie normalisée de SQL mais ajoutent au langage des extensions propriétaires qui exploitent certaines particularités du SGBD. Nous en avons déjà rencontré deux exemples : la génération automatique des clés primaires et les types autorisés pour les colonnes d'une table sont souvent dépendants du SGBD.

Les quatre commandes de base du langage SQL que nous présentons sont normalisées et acceptées par tous les SGBD :

select col1, col2,…
from table1, table2,…
where condition
order by expression
… 		La requête qui permet d'obtenir les données contenues dans une base. Seuls les mots clés de la première ligne sont obligatoires, les autres sont facultatifs. Il existe d'autres mots clés non présentés ici.
  • Une jointure est faite avec toutes les tables qui sont derrière le mot clé from
  • Seules les colonnes qui sont derrière le mot clé select sont conservées
  • Seules les lignes vérifiant la condition du mot clé where sont conservées
  • Les lignes résultantes ordonnées selon l'expression du mot clé order by forment le résultat de la requête. Ce résultat est une table.
insert into table(col1,col2…)
values (val1,val2…) 		Insère une ligne dans table. (col1, col2…) précise les colonnes de la ligne à initialiser avec les valeurs (val1, val2…).
update table
set col1=val1, col2=val2
where condition 		Met à jour les lignes de table vérifiant condition (toutes les lignes si pas de where). Pour ces lignes, la colonne coli reçoit la valeur vali
delete from table
where condition 		Supprime toutes les lignes de table vérifiant condition

Nous allons écrire une application console permettant d'émettre des ordres SQL sur la base [dbarticles] que nous avons créée précédemment. Voici un . Le lecteur est invité à comprendre les ordres SQL émis et leurs résultats.

Cacher Sélectionnez
  1. Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;Persist Security Info=True]
  2.
  3. Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles
  4.
  5. ------------------------------------
  6. ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
  7. ------------------------------------
  8.
  9. 1 vélo 500 10 5
  10. 2 pompe 10 10 2
  11. 3 arc 600 4 1
  12. 4 flèches - lot de 6 100 12 20
  13. 5 combinaison de plongée 300 8 2
  14. 6 bouteilles d'oxygène 120 10 5
  15.
  16. Requête SQL (rien pour arrêter) : insert into articles(nom,prix,stockactuel,stockminimum) values('x',100,10,1)
  17. Il y a eu 1 ligne(s) modifiée(s)
  18.
  19. Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles
  20.
  21. ------------------------------------
  22. ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
  23. ------------------------------------
  24.
  25. 1 vélo 500 10 5
  26. ...
  27. 6 bouteilles d'oxygène 120 10 5
  28. 9 x 100 10 1
  29.
  30. Requête SQL (rien pour arrêter) : update articles set prix=prix*1.1 where id=9
  31. Il y a eu 1 ligne(s) modifiée(s)
  32.
  33. Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles
  34.
  35. ------------------------------------
  36. ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
  37. ------------------------------------
  38.
  39. 1 vélo 500 10 5
  40. ...
  41. 6 bouteilles d'oxygène 120 10 5
  42. 9 x 110 10 1
  43.
  44. Requête SQL (rien pour arrêter) : delete from articles where id=9
  45. Il y a eu 1 ligne(s) modifiée(s)
  46.
  47. Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles
  48.
  49. ------------------------------------
  50. ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
  51. ------------------------------------
  52.
  53. 1 vélo 500 10 5
  54. ...
  55. 6 bouteilles d'oxygène 120 10 5
  • ligne 1 : la chaîne dite de connexion : elle contient tous les paramètres permettant de se connecter à la base de données.
  • ligne 3 : on demande le contenu de la table [articles]
  • ligne 16 : on insère une nouvelle ligne. On notera que le champ id n'est pas initialisé dans cette opération car c'est le SGBD qui va générer la valeur de ce champ.
  • ligne 19 : vérification. Ligne 28, la ligne a bien été ajoutée.
  • ligne 30 : on augmente de 10% le prix de l'article qui vient d'être ajouté.
  • ligne 33 : on vérifie
  • ligne 42 : l'augmentation du prix a bien eu lieu
  • ligne 44 : on supprime l'article qu'on a ajouté précédemment
  • ligne 47 : on vérifie
  • lignes 53-55 : l'article n'est plus là.

X-C-3. Les interfaces de base d'ADO.NET pour le mode connecté

Revenons au schéma d'une application exploitant une base de données au travers d'un connecteur ADO.NET :

Image non disponible

En mode connecté, l'application :

  • ouvre une connexion avec la source de données
  • travaille avec la source de données en lecture/écriture
  • ferme la connexion

Trois interfaces ADO.NET sont principalement concernées par ces opérations :

  • IDbConnection qui encapsule les propriétés et méthodes de la connexion.
  • IDbCommand qui encapsule les propriétés et méthodes de la commande SQL exécutée.
  • IDataReader qui encapsule les propriétés et méthodes du résultat d'un ordre SQL Select.

L'interface

Sert à gérer la connexion avec la base de données. Les méthodes M et propriétés P de cette interface que nous utiliserons seront les suivantes :

Nom | Type | Rôle
ConnectionString | P | chaîne de connexion à la base. Elle précise tous les paramètres nécessaires à l'établissement de la connexion avec une base précise.
Open | M | ouvre la connexion avec la base définie par ConnectionString
Close | M | ferme la connexion
BeginTransaction | M | démarre une transaction.
State | P | état de la connexion : ConnectionState.Closed , ConnectionState.Open , ConnectionState.Connecting , ConnectionState.Executing , ConnectionState.Fetching , ConnectionState.Broken

Si Connection est une classe implémentant l'interface IDbConnection , l'ouverture de la connexion peut se faire comme suit :

Cacher Sélectionnez
  1. IDbConnection connexion=new Connection();
  2. connexion.ConnectionString=...;
  3. connexion.Open();

L'interface

Sert à exécuter un ordre SQL ou une procédure stockée. Les méthodes M et propriétés P de cette interface que nous utiliserons seront les suivantes :

Nom | Type | Rôle
CommandType | P | indique ce qu'il faut exécuter - prend ses valeurs dans une énumération : - CommandType.Text : exécute l'ordre SQL défini dans la propriété CommandText . C'est la valeur par défaut. - CommandType.StoredProcedure : exécute une procédure stockée dans la base
CommandText | P | - le texte de l'ordre SQL à exécuter si CommandType = CommandType.Text - le nom de la procédure stockée à exécuter si CommandType = CommandType.StoredProcedure
Connection | P | la connexion IDbConnection à utiliser pour exécuter l'ordre SQL
Transaction | P | la transaction IDbTransaction dans laquelle exécuter l'ordre SQL
Parameters | P | la liste des paramètres d'un ordre SQL paramétré. L'ordre update articles set prix=prix*1.1 where id=@id a le paramètre @id.
ExecuteReader | M | pour exécuter un ordre SQL Select . On obtient un objet IDataReader représentant le résultat du Select .
ExecuteNonQuery | M | pour exécuter un ordre SQL Update, Insert, Delete . On obtient le nombre de lignes affectées par l'opération (mises à jour, insérées, détruites).
ExecuteScalar | M | pour exécuter un ordre SQL Select ne rendant qu'un unique résultat comme dans : select count(*) from articles .
CreateParameter | M | pour créer les paramètres IDbParameter d'un ordre SQL paramétré.
Prepare | M | permet d'optimiser l'exécution d'une requête paramétrée lorsqu'elle est exécutée de multiples fois avec des paramètres différents.

Si Command est une classe implémentant l'interface IDbCommand , l'exécution d'un ordre SQL sans transaction aura la forme suivante :

Cacher Sélectionnez
  1. // ouverture connexion
  2. IDbConnection connexion=...
  3. connexion.Open();
  4. // préparation commande
  5. IDbCommand commande=new Command();
  6. commande.Connection=connexion;
  7. // exécution ordre select
  8. commande.CommandText="select ...";
  9. IDbDataReader reader=commande.ExecuteReader();
  10. ...
  11. // exécution ordre update, insert, delete
  12. commande.CommandText="insert ...";
  13. int nbLignesInsérées=commande.ExecuteNonQuery();
  14. ...
  15. // fermeture connexion
  16. connexion.Close();

L'interface

Sert à encapsuler les résultats d'un ordre SQL Select . Un objet IDataReader représente une table avec des lignes et des colonnes, qu'on exploite séquentiellement : d'abord la 1re ligne, puis la seconde…. Les méthodes M et propriétés P de cette interface que nous utiliserons seront les suivantes :

Nom | Type | Rôle
FieldCount | P | le nombre de colonnes de la table IDataReader
GetName | M | GetName(i) rend le nom de la colonne n° i de la table IDataReader .
Item | P | Item[i] représente la colonne n° i de la ligne courante de la table IDataReader .
Read | M | passe à la ligne suivante de la table IDataReader . Rend le booléen True si la lecture a pu se faire, False sinon.
Close | M | ferme la table IDataReader .
GetBoolean | M | GetBoolean (i) : rend la valeur booléenne de la colonne n° i de la ligne courante de la table IDataReader . Les autres méthodes analogues sont les suivantes : GetDateTime , GetDecimal , GetDouble , GetFloat , GetInt16 , GetInt32 , GetInt64 , GetString .
Getvalue | M | Getvalue (i) : rend la valeur de la colonne n° i de la ligne courante de la table IDataReader en tant que type object .
IsDBNull | M | IsDBNull(i) rend True si la colonne n° i de la ligne courante de la table IDataReader n'a pas de valeur ce qui est symbolisé par la valeur SQL NULL.

L'exploitation d'un objet IDataReader ressemble souvent à ce qui suit :

Cacher Sélectionnez
  1. // ouverture connexion
  2. IDbConnection connexion=...
  3. connexion.Open();
  4. // préparation commande
  5. IDbCommand commande=new Command();
  6. commande.Connection=connexion;
  7. // exécution ordre select
  8. commande.CommandText="select ...";
  9. IDataReader reader=commande.ExecuteReader();
  10. // exploitation résultats
  11. while(reader.Read()){
  12.     // exploiter ligne courante
  13.         ...
  14. }
  15. // fermeture reader
  16. reader.Close();
  17. // fermeture connexion
  18. connexion.Close();

X-C-4. La gestion des erreurs

Revenons sur l'architecture d'une application avec base de données :

Image non disponible

La couche [dao] peut rencontrer de nombreuses erreurs lors de l'exploitation de la base de données. Celles-ci vont être remontées en tant qu'exceptions lancées par le connecteur ADO.NET. Le code de la couche [dao] doit les gérer. Toute opération avec la base de données doit se faire dans un try / catch / finally pour intercepter et gérer une éventuelle exception et libérer les ressources qui doivent l'être. Ainsi le code vu plus haut pour exploiter le résultat d'un ordre Select devient le suivant :

Cacher Sélectionnez
  1. // initialisation connexion
  2. IDbConnection connexion=...
  3. // exploitation connexion
  4. try{
  5.     // ouverture
  6.     connexion.Open();
  7.     // préparation commande
  8.     IDbCommand commande=new Command();
  9.     commande.Connection=connexion;
  10.     // exécution ordre select
  11.     commande.CommandText="select ...";
  12.     IDbDataReader reader=commande.ExecuteReader();
  13.     // exploitation résultats
  14.     try{
  15.         while(reader.Read()){
  16.         // exploiter ligne courante
  17.         ...
  18.     }finally{
  19.         // fermeture reader
  20.         reader.Close();
  21.     }
  22. }catch(Exception ex){
  23.     // gestion exception
  24.     ...
  25. }finally{
  26.     // fermeture connexion
  27.     connexion.Close();
  28. }
  29. ...

Quoiqu'il arrive, les objets IDataReader et IDbConnection doivent être fermés. C'est pourquoi cette fermeture est faite dans les clauses finally .

La fermeture de la connexion et celle du de l'objet IDataReader peuvent être automatisées avec une clause using :

Cacher Sélectionnez
  1. // exploitation connexion
  2. try{
  3.     using(IDbConnection connexion=...){
  4.         // ouverture
  5.         connexion.Open();
  6.         // préparation commande
  7.         IDbCommand commande=new Command();
  8.         commande.Connection=connexion;
  9.         // exécution ordre select
  10.         commande.CommandText="select ...";
  11.         using(IDbDataReader reader=commande.ExecuteReader()){
  12.             // exploitation résultats
  13.             while(reader.Read()){
  14.             // exploiter ligne courante
  15.             ...
  16.         }// using IData
  17.     }//using IDbConnection
  18. }catch(Exception ex){
  19.     // gestion exception
  20.     ...
  21. }
  22. ..
  • Ligne 3, la clause using nous assure que la connexion ouverte dans le bloc using(…){…} sera fermée en-dehors de celui-ci, ceci quelque soit la façon dont on sort du bloc : normalement ou par l'arrivée d'une exception. On économise un finally , mais l'intérêt n'est pas dans cette économie mineure. L'utilisation d'un using évite au développeur de fermer lui-même la connexion. Or oublier de fermer une connexion peut passer inaperçu et "planter" l'application d'une façon qui apparaîtra aléatoire, à chaque fois que le SGBD atteindra le nombre maximum de connexions ouvertes qu'il peut supporter.
  • Ligne 11 : on procède de façon analogue pour fermer l'objet IDataReader .

X-C-5. Configuration du projet exemple

Le projet final sera le suivant :

Image non disponible
  • [1] : le projet aura un fichier de configuration [App.config]
  • [2] : il utilise des classes de deux DLL non référencées par défaut et qu'il faut don ajouter aux références du projet :
  • [System.Configuration] pour exploiter le fichier de configuration [App.config]
  • [System.Data.SqlServerCe] pour exploiter la base de données Sql Server Compact
  • [3, 4] : rappellent comment ajouter des références à un projet.
  • [5, 6] : rappellent comment ajouter le fichier [App.config] à un projet.

Le fichier de configuration [App.config] sera le suivant :

Cacher Sélectionnez
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.  <connectionStrings>
  4.   <add name="dbSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
  5.  </connectionStrings>
  6. </configuration>
  • lignes 3-5 : la balise <connectionStrings> au pluriel définit des chaînes de connexion à des bases de données. Une chaîne de connexion à la forme "paramètre1=valeur1;paramètre2=valeur2;…". Elle définit tous les paramètres nécessaires à l'établissement d'une connexion avec une base de données particulière. Ces chaînes de connexion changent avec chaque SGBD. Le site [http://www.connectionstrings.com/] donne la forme de celles-ci pour les principaux SGBD.
  • ligne 4 : définit une chaîne de connexion particulière, ici celle de la base SQL Server Compact dbarticles.sdf que nous avons créée précédemment :
  • name = nom de la chaîne de connexion. C'est via ce nom qu'une chaîne de connexion est récupérée par le programme C#
  • connectionString : la chaîne de connexion pour une base SQL Server Compact
  • DataSource : désigne le chemin de la base. La syntaxe |DataDirectory| désigne le dossier d'exécution du projet.
  • Password : le mot de passe de la base. Ce paramètre est absent s'il n'y a pas de mot de passe.

Le code C# pour récupérer la chaîne de connexion précédente est le suivant :

Cacher Sélectionnez
  1. string connectionString = ConfigurationManager.ConnectionStrings["dbSqlServerCe"].ConnectionString;
  • ConfigurationManager est la classe de la DLL [System.Configuration] qui permet d'exploiter le fichier [App.config].
  • ConnectionsStrings["nom"].ConnectionString : désigne l'attribut connectionString de la balise < add name="nom" connectionString="…" > de la section < connectionStrings > de [App.config]

Le projet est désormais configuré. Nous étudions maintenant la classe [Program.cs] dont nous avons vu précédemment un exemple d'exécution.

X-C-6. Le programme exemple

Le programme [program.cs] est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Data.SqlServerCe;
  4. using System.Text;
  5. using System.Text.RegularExpressions;
  6. using System.Configuration;
  7.
  8. namespace Chap7 {
  9.     class SqlCommands {
  10.         static void Main(string[] args) {
  11.
  12.             // application console - exécute des requêtes SQL tapées au clavier
  13.             // sur une base de données dont la chaîne de connexion est obtenue dans un fichier de configuration
  14.
  15.             // exploitation du fichier de configuration [App.config]
  16.             string connectionString = null;
  17.             try {
  18.                 connectionString = ConfigurationManager.ConnectionStrings["dbSqlServerCe"].ConnectionString;
  19.             } catch (Exception e) {
  20.                 Console.WriteLine("Erreur de configuration : {0}", e.Message);
  21.                 return;
  22.             }
  23.
  24.             // affichage chaîne de connexion
  25.             Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);
  26.
  27.             // on construit un dictionnaire des commandes sql acceptées
  28.             string[] commandesSQL = new string[] { "select", "insert", "update", "delete" };
  29.             Dictionary<string, bool> dicoCommandes = new Dictionary<string, bool>();
  30.             for (int i = 0; i < commandesSQL.Length; i++) {
  31.                 dicoCommandes.Add(commandesSQL[i], true);
  32.             }
  33.
  34.             // lecture-exécution des commandes SQL tapées au clavier
  35.             string requête = null;    // texte de la requête SQL
  36.             string[] champs;            // les champs de la requête
  37.             Regex modèle = new Regex(@"\s+");    // suite d'espaces
  38.
  39.             // boucle de saisie-exécution des commandes SQL tapées au clavier
  40.             while (true) {
  41.                 // demande de la requête
  42.                 Console.Write("\nRequête SQL (rien pour arrêter) : ");
  43.                 requête = Console.ReadLine().Trim().ToLower();
  44.                 // fini ?
  45.                 if (requête == "")
  46.                     break;
  47.                 // on décompose la requête en champs
  48.                 champs = modèle.Split(requête);
  49.                 // requête valide ?
  50.                 if (champs.Length == 0 || ! dicoCommandes.ContainsKey(champs[0])) {
  51.                     // msg d'erreur
  52.                     Console.WriteLine("Requête invalide. Utilisez select, insert, update, delete ou rien pour arrêter");
  53.                     // requête suivante
  54.                     continue;
  55.                 }
  56.                 // exécution de la requête
  57.                 if (champs[0] == "select") {
  58.                     ExecuteSelect(connectionString, requête);
  59.                 } else
  60.                     ExecuteUpdate(connectionString, requête);
  61.             }
  62.         }
  63.
  64.         // exécution d'une requête de mise à jour
  65.         static void ExecuteUpdate(string connectionString, string requête) {
  66.     ...
  67.         }
  68.
  69.         // exécution d'une requête Select
  70.         static void ExecuteSelect(string connectionString, string requête) {
  71. ....
  72.         }
  73.     }
  74. }
  • lignes 1-6 : les espaces de nom utilisés dans l'application. La gestion d'une base de données SQL Server Compact nécessite l'espace de noms [System.Data.SqlServerCe] de la ligne 3. On a là une dépendance sur un espace de noms propriétaire à un SGBD. On peut en déduire que le programme devra être modifié si on change de SGBD.
  • ligne 18 : la chaîne de connexion à la base est lue dans le fichier [App.config] et affichée ligne 25. Elle servira pour l'établissement d'une connexion avec la base de données.
  • lignes 28-32 : un dictionnaire mémorisant les noms des quatre ordres SQL autorisés : select, insert, update, delete.
  • lignes 40-62 : la boucle de saisie des ordres SQL tapés au clavier et leur exécution sur la base de données
  • ligne 48 : la ligne tapée au clavier est décomposée en champs afin d'en connaître le premier terme qui doit être : select, insert, update, delete
  • lignes 50-55 : si la requête est invalide, un message d'erreur est affiché et on passe à la requête suivante.
  • lignes 57-61 : on exécute l'ordre SQL saisi. Cette exécution prend une forme différente selon que l'on a affaire à un ordre select ou à un ordre insert , update , delete . Dans le premier cas, l'ordre ramène des données de la base sans modifier celle-ci, dans le second il la met à jour sans ramener des données. Dans les deux cas, on délègue l'exécution à une méthode qui a besoin de deux paramètres :
  • la chaîne de connexion qui va lui permettre de se connecter à la base
  • l'ordre SQL à exécuter sur cette connexion

X-C-7. Exécution d'une requête SELECT

L'exécution d'ordres SQL nécessite les étapes suivantes :

  • Connexion à la base de données
  • Émission des ordres SQL vers la base
  • Traitement des résultats de l'ordre SQL
  • Fermeture de la connexion

Les étapes 2 et 3 sont réalisées de façon répétée, la fermeture de connexion n'ayant lieu qu'à la fin de l'exploitation de la base. Les connexions ouvertes sont des ressources limitées d'un SGBD. Il faut les économiser. Aussi cherchera-t-on toujours à limiter la durée de vie d'une connexion ouverte. Dans l'exemple étudié, la connexion est fermée après chaque ordre SQL. Une nouvelle connexion est ouverte pour l'ordre SQL suivant. L'ouverture / fermeture d'une connexion est coûteuse. Pour diminuer ce coût, certains SGBD offrent la notion de pools de connexions ouvertes : lors de l'initialisation de l'application, N connexions sont ouvertes et sont affectées au pool. Elles resteront ouvertes jusqu'à la fin de l'application. Lorsque l'application ouvre une connexion, elle reçoit l'une des N connexions déjà ouvertes du pool. Lorsqu'elle ferme la connexion, celle-ci est simplement remise dans le pool. L'intérêt de ce système est qu'il est transparent pour le développeur : le programme n'a pas à être modifié pour utiliser le pool de connexions. La configuration du pool de connexions est dépendant du SGBD.

Nous nous intéressons tout d'abord à l'exécution des ordres SQL Select . La méthode ExecuteSelect de notre programme exemple est la suivante :

Cacher Sélectionnez
  1. // exécution d'une requête Select
  2.         static void ExecuteSelect(string connectionString, string requête) {
  3.             // on gère les éventuelles exceptions
  4.             try {
  5.                 using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
  6.                     // ouverture connexion
  7.                     connexion.Open();
  8.                     // exécute sqlCommand avec requête select
  9.                     SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
  10.                     SqlCeDataReader reader= sqlCommand.ExecuteReader();
  11.                     // affichage résultats
  12.                     AfficheReader(reader);
  13.                 }
  14.             } catch (Exception ex) {
  15.                 // msg d'erreur
  16.                 Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
  17.             }
  18.         }
  19.
  20.         // affichage reader
  21.         static void AfficheReader(IDataReader reader) {
  22. ...
  23.         }
  • ligne 2 : la méthode reçoit deux paramètres :
  • ligne 4 : toute opération avec une base de données peut générer une exception qu'on peut vouloir gérer. C'est d'autant plus important ici que les ordres SQL donnés par l'utilisateur peuvent être syntaxiquement erronés. Il faut qu'on puisse le lui dire. Tout le code est donc à l'intérieur d'un try / catch .
  • ligne 5 : il y a plusieurs choses ici :
  • la connexion avec la base est initialisée avec la chaîne de connexion [connectionString]. Elle n'est pas encore ouverte. Elle le sera ligne 7.
  • la clause using (Ressource) {…} est une facilité syntaxique garantissant la libération de la ressource Ressource , ici une connexion, à la sortie du bloc contrôlé par le using .
  • la connexion est d'un type propriétaire : SqlCeConnection , propre au SGBD SQL Server Compact.
  • ligne 7 : la connexion est ouverte. C'est à ce moment que les paramètres de la chaîne de connexion sont utilisés.
  • ligne 9 : un ordre SQL est émis via un objet propriétaire SqlCeCommand . La ligne 9 initialise cet objet avec deux informations : la connexion à utiliser et l'ordre SQL à émettre dessus. L'objet SqlCeCommand sert aussi bien à exécuter un ordre Select qu'un ordre Update , Insert , Delete . Ses propriétés et méthodes ont été présentées page .
  • ligne 10 : un ordre SQL Select est exécuté via la méthode ExecuteReader de l'objet SqlCeCommand qui rend un objet IDataReader dont a présenté les méthodes et propriétés page .
  • ligne 12 : l'affichage des résultats est confiée à la méthode AfficheReader suivante :
Cacher Sélectionnez
  1.         // affichage reader
  2.         static void AfficheReader(IDataReader reader) {
  3.             using (reader) {
  4.                 // exploitation des résultats
  5.                 // -- colonnes
  6.                 StringBuilder ligne = new StringBuilder();
  7.                 int i;
  8.                 for (i = 0; i < reader.FieldCount - 1; i++) {
  9.                     ligne.Append(reader.GetName(i)).Append(",");
  10.                 }
  11.                 ligne.Append(reader.GetName(i));
  12.                 Console.WriteLine("\n{0}\n{1}\n{2}\n", "".PadLeft(ligne.Length, '-'), ligne, "".PadLeft(ligne.Length, '-'));
  13.                 // -- données
  14.                 while (reader.Read()) {
  15.                     // exploitation ligne courante
  16.                     ligne = new StringBuilder();
  17.                     for (i = 0; i < reader.FieldCount; i++) {
  18.                         ligne.Append(reader[i].ToString()).Append(" ");
  19.                     }
  20.                     Console.WriteLine(ligne);
  21.                 }
  22.             }
  23. }
  • ligne 2 : la méthode reçoit un objet IDataReader . On notera qu'ici nous avons utilisé une interface et non une classe spécifique.
  • ligne 3 : la clause using est utilisée pour gérer de façon automatique la fermeture de l'objet IDataReader .
  • lignes 8-10 : on affiche les noms des colonnes de la table résultat du Select . Ce sont les colonnes coli de la requête select col1, col2… from table …
  • lignes 14-21 : on parcourt la table des résultats et on affiche les valeurs de chaque ligne de la table.
  • ligne 18 : on ne connaît pas le type de la colonne n° i du résultat parce qu'on ne connaît pas la table interrogée. On ne peut donc utiliser la syntaxe reader.GetXXX(i) où XXX est le type de la colonne n° i, car on ne connaît pas ce type. On utilise alors la syntaxe reader.Item[i].ToString() pour avoir la représentation de la colonne n° i sous forme de chaîne de caractères. La syntaxe reader.Item[i].ToString() peut être abrégée en reader[i].ToString().

X-C-8. Exécution d'un ordre de mise à jour : INSERT, UPDATE, DELETE

Le code de la méthode ExecuteUpdate est le suivant :

Cacher Sélectionnez
  1.         // exécution d'une requête de mise à jour
  2.         static void ExecuteUpdate(string connectionString, string requête) {
  3.             // on gère les éventuelles exceptions
  4.             try {
  5.                 using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
  6.                     // ouverture connexion
  7.                     connexion.Open();
  8.                     // exécute sqlCommand avec requête de mise à jour
  9.                     SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
  10.                     int nbLignes = sqlCommand.ExecuteNonQuery();
  11.                     // affichage résultat
  12.                     Console.WriteLine("Il y a eu {0} ligne(s) modifiée(s)", nbLignes);
  13.                 }
  14.             } catch (Exception ex) {
  15.                 // msg d'erreur
  16.                 Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
  17.             }
  18. }

Nous avons dit que l'exécution d'un ordre d'interrogation Select ne différait de celle d'un ordre de mise à jour Update , Insert , Delete que par la méthode de l'objet SqlCeCommand utilisée : ExecuteReader pour Select , ExecuteNonQuery pour Update , Insert , Delete . Nous ne commentons que cette dernière méthode dans le code ci-dessus :

  • ligne 10 : l'ordre Update , Insert , Delete est exécuté par la méthode ExecuteNonQuery de l'objet SqlCeCommand . Si elle réussit, cette méthode rend le nombre de lignes mises à jour (update) ou insérées (insert) ou détruites (delete).
  • ligne 12 : ce nombre de lignes est affiché à l'écran

Le lecteur est invité à revoir un exemple d'exécution de ce code, page .

X-D. Autres connecteurs ADO.NET

Le code que nous avons étudié est propriétaire : il dépend de l'espace de noms [System.Data.SqlServerCe] destiné au SGBD SQL Server Compact. Nous allons maintenant construire le même programme avec différents connecteurs .NET et voir ce qui change.

X-D-1. Connecteur SQL Server 2005

L'architecture utilisée sera la suivante :

Image non disponible

L'installation de SQL Server 2005 est décrite en annexes au paragraphe , page .

Nous créons un second projet dans la même solution que précédemment puis nous créons la base de données SQL server 2005. Le SGBD SQL Server 2005 doit être lancé avant les opérations qui suivent :

Image non disponible
  • [1] : créer un nouveau projet dans la solution actuelle et en faire le projet courant.
  • [2] : créer une nouvelle connexion
  • [3] : choisir le type de connexion
Image non disponible
  • [4] : choisir le SGBD SQL Server
  • [5] : résultat du choix précédent
  • [6] : utiliser le bouton [Browse] pour indiquer où créer la base SQL Server 2005. La base de données est encapsulée dans un fichier .mdf.
  • [7] : choisir la racine du nouveau projet et appeler la base [dbarticles.mdf].
  • [8] : utiliser une authentification windows.
  • [9] : valider la page de renseignements
Image non disponible
  • [11] : la base SQL Server
  • [12] : créer une table. Celle-ci sera identique à la base SQL Server Compact construite précédemment.
  • [13] : le champ [id]
  • [14] : le champ [id] est de type Identity .
  • [15,16] : le champ [id] est clé primaire
Image non disponible
  • [17] : les autres champs de la table
  • [18] : donner le nom [articles] à la table au moment de sa sauvegarde (Ctrl+S).

Il nous reste à mettre des données dans la table :

Image non disponible

Nous incluons la base de données dans le projet :

Image non disponible

Les références du projet sont les suivantes :

Image non disponible

Le fichier de configuration [App.config] est le suivant :

Cacher Sélectionnez
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.  <connectionStrings>
  4.   <add name="connectString1" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Integrated Security=True;Connect Timeout=30;User Instance=True;" />
  5.      <add name="connectString2" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;Connect Timeout=30;" />
  6.  </connectionStrings>
  7. </configuration>
  • ligne 4 : la chaîne de connexion à la base [dbarticles.mdf] avec une authentification windows
  • ligne 5 : la chaîne de connexion à la base [dbarticles.mdf] avec une authentification SQL Server. [sa,msde] est le couple (login,password) de l'administrateur du serveur SQL Server tel que défini au paragraphe , page .

Le programme [Program.cs] évolue de la façon suivante :

Cacher Sélectionnez
  1. using System.Data.SqlClient;
  2. ...
  3.
  4. namespace Chap7 {
  5.     class SqlCommands {
  6.         static void Main(string[] args) {
  7. ...
  8.             // exploitation du fichier de configuration [App.config]
  9.             string connectionString = null;
  10.             try {
  11.                 connectionString = ConfigurationManager.ConnectionStrings["connectString2"].ConnectionString;
  12.             } catch (Exception e) {
  13. ...
  14.             }
  15. ...
  16.             // lecture-exécution des commandes SQL tapées au clavier
  17. ...
  18.         }
  19.
  20.         // exécution d'une requête de mise à jour
  21.         static void ExecuteUpdate(string connectionString, string requête) {
  22.             // on gère les éventuelles exceptions
  23.             try {
  24.                 using (SqlConnection connexion = new SqlConnection(connectionString)) {
  25.                     // ouverture connexion
  26.                     connexion.Open();
  27.                     // exécute sqlCommand avec requête de mise à jour
  28.                     SqlCommand sqlCommand = new SqlCommand(requête, connexion);
  29.                     int nbLignes = sqlCommand.ExecuteNonQuery();
  30.                     // affichage résultat
  31.                     Console.WriteLine("Il y a eu {0} ligne(s) modifiée(s)", nbLignes);
  32.                 }
  33.             } catch (Exception ex) {
  34. ....
  35.             }
  36.         }
  37.
  38.         // exécution d'une requête Select
  39.         static void ExecuteSelect(string connectionString, string requête) {
  40.             // on gère les éventuelles exceptions
  41.             try {
  42.                 using (SqlConnection connexion = new SqlConnection(connectionString)) {
  43.                     // ouverture connexion
  44.                     connexion.Open();
  45.                     // exécute sqlCommand avec requête select
  46.                     SqlCommand sqlCommand = new SqlCommand(requête, connexion);
  47.                     SqlDataReader reader = sqlCommand.ExecuteReader();
  48.                     // exploitation des résultats
  49. ...
  50.                 }
  51.             } catch (Exception ex) {
  52. ...
  53.             }
  54.         }
  55.     }
  56. }
  • ligne 1 : l'espace de noms [System.Data.SqlClient] contient les classes permettant de gérer une base SQL Server 2005
  • ligne 24 : la connexion est de type SQLConnection
  • ligne 28 : l'objet encapsulant les ordres SQL est de type SQLCommand
  • ligne 47 : l'objet encapsulant le résultat d'un ordre SQL Select est de type SQLDataReader

Le code est identique à celui utilisé avec le SGBD SQL Server Compact au nom des classes près. Pour l'exécuter, on peut utiliser (ligne 11) l'une ou l'autre des deux chaînes de connexion définies dans [App.config].

X-D-2. Connecteur MySQL5

L'architecture utilisée sera la suivante :

Image non disponible

L'installation de MySQL5 est décrite en annexes au paragraphe , page et celle du connecteur Ado.Net au paragraphe , page .

Nous créons un troisième projet dans la même solution que précédemment et nous lui ajoutons les références dont il a besoin :

Image non disponible
  • [1] : le nouveau projet
  • [2] : auquel on ajoute des références
  • [3] : la DLL [MySQL.Data] du connecteur Ado.Net de MySql5 ainsi que celle de [System.Configuration] [4].

Nous créons maintenant la base de données [dbarticles] et sa table [articles]. Le SGBD MySQL5 doit être lancé. Par ailleurs, on lance le client [Query Browser] (cf paragraphe , page ).

Image non disponible
  • [1] : dans [Query Browser], cliquer droit dans la zone [Schemata] [2] pour créer [3] un nouveau schéma, terme qui désigne une base de données.
  • [4] : la base de données s'appellera [dbarticles]. En [5], on la voit. Elle est pour l'instant sans tables. Nous allons exécuter le script SQL suivant :
Cacher Sélectionnez
  1. /* choix de la base de données courante */
  2. USE dbarticles;
  3. /* création de la table des articles */
  4. CREATE TABLE ARTICLES (
  5.     ID            INTEGER PRIMARY KEY AUTO_INCREMENT,
  6.     NOM           VARCHAR(20) NOT NULL,
  7.     PRIX          DOUBLE PRECISION NOT NULL,
  8.     STOCKACTUEL   INTEGER NOT NULL,
  9.     STOCKMINIMUM  INTEGER NOT NULL
  10. );
  11. /* insertion de données dans la table */
  12. INSERT INTO ARTICLES (NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES ('article1', 100, 10, 1);
  13. INSERT INTO ARTICLES (NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES ('article2', 200, 20, 2);
  14. INSERT INTO ARTICLES (NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES ('article3', 300, 30, 3);
  15. /* ajout de contraintes */
  16. ALTER TABLE ARTICLES ADD CONSTRAINT CHK_ID check (ID>0);
  17. ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRIX check (PRIX>0);
  18. ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKACTUEL check (STOCKACTUEL>0);
  19. ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM>0);
  20. ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NOM check (NOM<>'');
  21. ALTER TABLE ARTICLES ADD CONSTRAINT UNQ_NOM UNIQUE (NOM);
  • ligne 1 : la base [dbarticles] devient la base courante. Les ordres SQL qui suivent s'exécuteront sur elle.
  • lignes 4-10 : définition de la table [ARTICLES]. On notera le SQL propriétaire de MySQL. Les types des colonnes, la génération automatique de la clé primaire (attribut AUTO_INCREMENT) diffèrent de ce qui a été rencontré avec les SGBD SQL Server Compact et Express.
  • lignes 12-14 : insertion de trois lignes
  • lignes 16-21 : ajout de contraintes d'intégrité sur les colonnes.

Ce script est exécuté dans [MySQL Query Browser] :

Image non disponible
  • dans [MySQL Query Browser] [6], on charge le script [7]. On le voit en [8]. En [9], il est exécuté.
Image non disponible
  • en [10], la table [articles] a été créée. On double-clique dessus. Cela fait apparaître la fenêtre [11] avec la requête [12] dedans prête à être exécutée par [13]. En [14], le résultat de l'exécution. On a bien les trois lignes attendues. On notera que les valeurs du champ [ID] ont été générées automatiquement (attribut AUTO_INCREMENT du champ).

Maintenant que la base de données est prête, nous pouvons revenir au développement de l'application dans Visual studio.

Image non disponible

En [1], le programme [Program.cs] et le fichier de configuration [App.config]. Celui-ci est le suivant :

Cacher Sélectionnez
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.  <connectionStrings>
  4.      <add name="dbArticlesMySql5" connectionString="Server=localhost;Database=dbarticles;Uid=root;Pwd=root;" />
  5.  </connectionStrings>
  6. </configuration>

Ligne 4, les éléments de la chaîne de connexion sont les suivants :

  • Server : nom de la machine sur laquelle se trouve le SGBD MySQL, ici localhost , c.a.d. la machine sur laquelle va être exécutée le programme.
  • Database : le nom de la base de données gérée, ici dbarticles
  • Uid : le login de l'utilisateur, ici root
  • Pwd : son mot de passe, ici root . Ces deux informations désignent l'administrateur créé au paragraphe , page .

Le programme [Program.cs] est identique à celui des versions précédentes aux détails près suivants :

espace de noms | MySql.Data.MySqlClient
classe Connection | MySqlConnection
classe Command | MySqlCommand
classe DataReader | MySqlDataReader

Le programme utilise la chaîne de connexion nommée dbArticlesMySql5 dans le fichier [App.config]. L'exécution donne les résultats suivants :

Cacher Sélectionnez
  1. Chaîne de connexion à la base : [Server=localhost;Database=dbarticles;Uid=root;Pwd=root;]
  2.
  3. Requête SQL (rien pour arrêter) : select * from articles
  4.
  5. ------------------------------------
  6. ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
  7. ------------------------------------
  8.
  9. 1 article1 100 10 1
  10. 2 article2 200 20 2
  11. 3 article3 300 30 3

X-D-3. Connecteur ODBC

L'architecture utilisée sera la suivante :

Image non disponible

L'intérêt des connecteurs ODBC est qu'ils présentent une interface standard aux applications qui les utilisent. Ainsi la nouvelle application va-t-elle, avec un unique code, pouvoir dialoguer avec tout SGBD ayant un connecteur ODBC, c.a.d. la plupart des SGBD. Les performances des connecteurs ODBC sont moins bonnes que celles des connecteurs "propriétaires" qui savent exploiter toutes les caractéristiques d'un SGBD particulier. En contre-partie, on obtient une grande souplesse de l'application : on peut changer de SGBD sans changer le code.

Nous étudions un exemple où l'application exploite une base MySQL5 ou une base SQL server Express selon la chaîne de connexion qu'on lui donne. Dans ce qui suit, nous supposons que :

  • les SGBD SQL Server Express et MySQL5 ont été lancés
  • que le pilote ODBC de MySQL5 est présent sur la machine (cf paragraphe , page ). Celui de SQL Server 2005 est présent par défaut.
  • les bases de données utilisées sont celles du paragraphe , page pour la base MySQL5, celle du paragraphe , page pour la base SQL Server Express.

Le nouveau projet Visual studio est le suivant :

Image non disponible

Ci-dessus, la base SQL Server [dbarticles.mdf] créée au paragraphe , page a été recopiée dans le dossier du projet.

Le fichier de configuration [App.config] est le suivant :

Cacher Sélectionnez
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.  <connectionStrings>
  4.      <add name="dbArticlesOdbcMySql5" connectionString="Driver={MySQL ODBC 3.51 Driver};Server=localhost;Database=dbarticles; User=root;Password=root;" />
  5.      <add name="dbArticlesOdbcSqlServer2005" connectionString="Driver={SQL Native Client};Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
  6.  </connectionStrings>
  7. </configuration>
  • ligne 4 : la chaîne de connexion de la source ODBC MySQL5. C'est une chaîne déjà étudiée dans laquelle on trouve un nouveau paramètre Driver qui définit le pilote ODBC à utiliser.
  • ligne 5 : la chaîne de connexion de la source ODBC SQL Server Express. C'est la chaîne déjà utilisée dans un exemple précédent à laquelle le paramètre Driver a été ajouté.

Le programme [Program.cs] est identique à celui des versions précédentes aux détails près suivants :

espace de noms | System.Data.Odbc
classe Connection | OdbcConnection
classe Command | OdbcCommand
classe DataReader | OdbcDataReader

Le programme utilise l'une des deux chaînes de connexion définies dans le fichier [App.config]. L'exécution donne les résultats suivants :

Avec la chaîne de connexion [dbArticlesOdbcSqlServer2005] :

Cacher Sélectionnez
  1. Chaîne de connexion à la base : [Driver={SQL Native Client};Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;]
  2.
  3. Requête SQL (rien pour arrêter) : select * from articles
  4.
  5. ------------------------------------
  6. id,nom,prix,stockactuel,stockminimum
  7. ------------------------------------
  8.
  9. 1 vélo 500,0000 10 5
  10. 2 pompe 10,0000 10 2
  11. 3 arc 610,0000 4 1
  12. 4 flèches - lot de 6 100,0000 12 20
  13. 5 combinaison de plongée 300,0000 8 2
  14. 6 Bouteilles d'oxygène 120,0000 10 5

Avec la chaîne de connexion [dbArticlesOdbcMySql5] :

Cacher Sélectionnez
  1. Chaîne de connexion à la base : [Driver={MySQL ODBC 3.51 Driver};Server=localhost;Database=dbarticles; User=root;Password=root;]
  2.
  3. Requête SQL (rien pour arrêter) : select * from articles
  4.
  5. ------------------------------------
  6. ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
  7. ------------------------------------
  8.
  9. 1 article1 100 10 1
  10. 2 article2 200 20 2
  11. 3 article3 300 30 3

X-D-4. Connecteur OLE DB

L'architecture utilisée sera la suivante :

Image non disponible

Comme les connecteurs ODBC, les connecteurs OLE DB (Object Linking and Embedding DataBase) présentent une interface standard aux applications qui les utilisent. Les pilotes ODBC permettent l'accès à des bases de données. Les sources de données pour les pilotes OLE DB sont plus variées : bases de données, messageries, annuaires… Toute source de données peut faire l'objet d'un pilote Ole DB si un éditeur le décide. On a ainsi un accès standard à une grande variété de données.

Nous étudions un exemple où l'application exploite une base ACCESS ou une base SQL server Express selon la chaîne de connexion qu'on lui donne. Dans ce qui suit, nous supposons que le SGBD SQL Server Express a été lancé et que la base de données utilisée est celle de l'exemple précédent.

Le nouveau projet Visual studio est le suivant :

Image non disponible
  • en [1] : l'espace de noms nécessaire aux connecteurs OLE DB est [System.Data.OleDb] présent dans la référence [System.Data] ci-dessus. La base SQL Server [dbarticles.mdf] a été recopiée à partir du projet précédent. La base [dbarticles.mdb] a été créée avec Access.
  • en [2] : comme la base SQL Server, la base ACCESS a la propriété [Copy to Output Directory=Copy Always] afin qu'elle soit automatiquement recopiée dans le dossier d'exécution du projet.

La base de données ACCESS [dbarticles.mdb] est la suivante :

Image non disponible

En [1], la structure de la table [articles] et en [2] son contenu.

Le fichier de configuration [App.config] est le suivant :

Cacher Sélectionnez
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.  <connectionStrings>
  4.      <add name="dbArticlesOleDbAccess"  connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\dbarticles.mdb;"/>
  5.      <add name="dbArticlesOleDbSqlServer2005" connectionString="Provider=SQLNCLI;Server=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
  6.  </connectionStrings>
  7. </configuration>
  • ligne 4 : la chaîne de connexion de la source OLE DB ACCESS. On y trouve le paramètre Provider qui définit le pilote OLE DB à utiliser ainsi que le chemin de la base de données
  • ligne 5 : la chaîne de connexion de la source OLE DB Server Express.

Le programme [Program.cs] est identique à celui des versions précédentes aux détails près suivants :

espace de noms | System.Data.OleDb
classe Connection | OleDbConnection
classe Command | OleDbCommand
classe DataReader | OleDbDataReader

Le programme utilise l'une des deux chaînes de connexion définies dans le fichier [App.config]. L'exécution donne les résultats suivants avec la chaîne de connexion [dbArticlesOleDbAccess] :

Cacher Sélectionnez
  1. Chaîne de connexion à la base : [Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\dbarticles.mdb;]
  2.
  3. Requête SQL (rien pour arrêter) : select * from articles
  4.
  5. ------------------------------------
  6. id,nom,prix,stockactuel,stockminimum
  7. ------------------------------------
  8.
  9. 1 vélo 1202 5 2
  10. 2 arc 5000 10 2
  11. 3 canoé 1502 12 6
  12. 4 fusil 3000 10 2
  13. 5 skis nautiques 1800 5 2
  14. 6 essai3 3 3 3
  15. 7 cachalot 200000 1 0
  16. 8 léopard 500000 1 1
  17. 9 panthère 800000 1 1

X-D-5. Connecteur générique

L'architecture utilisée sera la suivante :

Image non disponible

Comme les connecteurs ODBC et OLE DB, le connecteur générique présente une interface standard aux applications qui l'utilise mais améliore les performances sans sacrifier la souplesse. En effet, le connecteur générique s'appuie sur les connecteurs propriétaires des SGBD. L'application utilise des classes du connecteur générique. Ces classes servent d'intermédiaires entre l'application et le connecteur propriétaire.

Ci-dessus, lorsque l'application demande par exemple une connexion au connecteur générique, celui-ci lui rend une instance IDbConnection, l'interface des connexions décrite page , implémentée par une classe MySQLConnection ou SQLConnection selon la nature de la demande qui lui a été faite. On dit que le connecteur générique a des classes de type factory : on utilise une classe factory pour lui demander de créer des objets et en donner des références (pointeurs). D'où son nom (factory=usine, usine de production d'objets).

Il n'existe pas de connecteur générique pour tous les SGBD (avril 2008). Pour connaître ceux installés sur une machine, on pourra utiliser le programme suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Data;
  3. using System.Data.Common;
  4.
  5. namespace Chap7 {
  6.     class Providers {
  7.         public static void Main() {
  8.             DataTable dt = DbProviderFactories.GetFactoryClasses();
  9.             foreach (DataColumn col in dt.Columns) {
  10.                 Console.Write("{0}|", col.ColumnName);
  11.             }
  12.             Console.WriteLine("\n".PadRight(40, '-'));
  13.             foreach (DataRow row in dt.Rows) {
  14.                 foreach (object item in row.ItemArray) {
  15.                     Console.Write("{0}|", item);
  16.                 }
  17.                 Console.WriteLine("\n".PadRight(40, '-'));
  18.             }
  19.         }
  20.     }
  21. }
  • ligne 8 : la méthode statique [DbProviderFactories.GetFactoryClasses()] rend la liste des connecteurs génériques installés, sous la forme d'une table de base de données placée en mémoire (DataTable).
  • lignes 9-11 : affichent les noms des colonnes de la table dt :
  • dt.Columns est la liste des colonnes de la table. Une colonne C est de type DataColumn
  • [DataColumn].ColumnName est le nom de la colonne
  • lignes 13-18 : affichent les lignes de la table dt :
  • dt.Rows est la liste des lignes de la table. Une ligne L est de type DataRow
  • [DataRow].ItemArray est un tableau d'objets ou chaque objet représente une colonne de la ligne

Le résultat de l'exécution sur ma machine est le suivant :

Cacher Sélectionnez
  1. Name|Description|InvariantName|AssemblyQualifiedName|
  2. ---------------------------------------
  3. Odbc Data Provider|.Net Framework Data Provider for Odbc|System.Data.Odbc|System.Data.Odbc.OdbcFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
  4. ---------------------------------------
  5. OleDb Data Provider|.Net Framework Data Provider for OleDb|System.Data.OleDb|System.Data.OleDb.OleDbFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
  6. ---------------------------------------
  7. OracleClient Data Provider|.Net Framework Data Provider for Oracle|System.Data.OracleClient|System.Data.OracleClient.OracleClientFactory, System.Data.OracleClient, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
  8. ---------------------------------------
  9. SqlClient Data Provider|.Net Framework Data Provider for SqlServer|System.Data.SqlClient|System.Data.SqlClient.SqlClientFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
  10. ---------------------------------------
  11. Microsoft SQL Server Compact Data Provider|.NET Framework Data Provider for Microsoft SQL Server Compact|System.Data.SqlServerCe.3.5|System.Data.SqlServerCe.SqlCeProviderFactory, System.Data.SqlServerCe, Version=3.5.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91|
  12. ---------------------------------------
  13. MySQL Data Provider|.Net Framework Data Provider for MySQL|MySql.Data.MySqlClient|MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=5.2.1.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d|
  • ligne 1 : la table a quatre colonnes. Les trois premières sont les plus utiles pour nous ici.

L'affichage suivant montre que l'on dispose des connecteurs génériques suivants :

Nom | Identifiant
Odbc Data Provider | System.Data.Odbc
OleDb Data Provider | System.Data.OleDb
OracleClient Data Provider | System.Data.OracleClient
SqlClient Data Provider | System.Data.SqlClient
Microsoft SQL Server Compact Data Provider | System.Data.SqlServerCe.3.5
MySQL Data Provider | MySql.Data.MySqlClient

Un connecteur générique est accessible dans un programme C# via son identifiant.

Nous étudions un exemple où l'application exploite les diverses bases de données que nous avons construites jusqu'à maintenant. L'application recevra deux paramètres :

  • un premier paramètre précise le type de SGBD utilisé afin que la bonne bibliothèque de classes soit utilisée
  • le second paramètre précise la base de données gérée, via une chaîne de connexion.

Le nouveau projet Visual studio est le suivant :

Image non disponible
  • en [1] : l'espace de noms nécessaire aux connecteurs génériques est [System.Data.common] présent dans la référence [System.Data].

Le fichier de configuration [App.config] est le suivant :

Cacher Sélectionnez
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.     <connectionStrings>
  4.         <add name="dbArticlesSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
  5.         <add name="dbArticlesSqlServer" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
  6.         <add name="dbArticlesMySql5" connectionString="Server=localhost;Database=dbarticles;Uid=root;Pwd=root;" />
  7.         <add name="dbArticlesOdbcMySql5" connectionString="Driver={MySQL ODBC 3.51 Driver};Server=localhost;Database=dbarticles; User=root;Password=root;Option=3;" />
  8.         <add name="dbArticlesOleDbSqlServer2005" connectionString="Provider=SQLNCLI;Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
  9.         <add name="dbArticlesOdbcSqlServer2005" connectionString="Driver={SQL Native Client};Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
  10.         <add name="dbArticlesOleDbAccess"  connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\dbarticles.mdb;Persist Security Info=True"/>
  11.     </connectionStrings>
  12.     <appSettings>
  13.         <add key="factorySqlServerCe" value="System.Data.SqlServerCe.3.5"/>
  14.         <add key="factoryMySql" value="MySql.Data.MySqlClient"/>
  15.         <add key="factorySqlServer" value="System.Data.SqlClient"/>
  16.         <add key="factoryOdbc" value="System.Data.Odbc"/>
  17.         <add key="factoryOleDb" value="System.Data.OleDb"/>
  18.     </appSettings>
  19. </configuration>
  • lignes 3-11 : les chaînes de connexion des diverses bases de données exploitées.
  • lignes 13-17 : les noms des connecteurs génériques à utiliser

Le programme [Program.cs] est le suivant :

Cacher Sélectionnez
  1. ...
  2. using System.Data.Common;
  3.
  4. namespace Chap7 {
  5.     class SqlCommands {
  6.         static void Main(string[] args) {
  7.
  8.             // application console - exécute des requêtes SQL tapées au clavier
  9.             // sur une base de données dont la chaîne de connexion est obtenue dans un fichier de configuration ainsi que le nom du connecteur du SGBD associé
  10.
  11.             // vérification paramètres
  12.             if (args.Length != 2) {
  13.                 Console.WriteLine("Syntaxe : pg factory connectionString");
  14.                 return;
  15.             }
  16.
  17.             // exploitation du fichier de configuration
  18.             string factory = null;
  19.             string connectionString = null;
  20.             DbProviderFactory connecteur = null;
  21.             try {
  22.                 // factory
  23.                 factory = ConfigurationManager.AppSettings[args[0]];
  24.                 // chaîne de connexion
  25.                 connectionString = ConfigurationManager.ConnectionStrings[args[1]].ConnectionString;
  26.                 // on récupère un connecteur générique pour le SGBD
  27.                 connecteur = DbProviderFactories.GetFactory(factory);
  28.             } catch (Exception e) {
  29.                 Console.WriteLine("Erreur de configuration : {0}", e.Message);
  30.                 return;
  31.             }
  32.
  33.             // affichages
  34.             Console.WriteLine("Provider factory : [{0}]\n", factory);
  35.             Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);
  36.
  37. ...
  38.                 // exécution de la requête
  39.                 if (champs[0] == "select") {
  40.                     ExecuteSelect(connecteur,connectionString, requête);
  41.                 } else
  42.                     ExecuteUpdate(connecteur, connectionString, requête);
  43.             }
  44.         }
  45.
  46.         // exécution d'une requête de mise à jour
  47.         static void ExecuteUpdate(DbProviderFactory connecteur, string connectionString, string requête) {
  48.             // on gère les éventuelles exceptions
  49.             try {
  50.                 using (DbConnection connexion = connecteur.CreateConnection()) {
  51.                     // configuration connexion
  52.                     connexion.ConnectionString = connectionString;
  53.                     // ouverture connexion
  54.                     connexion.Open();
  55.                     // configuration Command
  56.                     DbCommand sqlCommand = connecteur.CreateCommand();
  57.                     sqlCommand.CommandText = requête;
  58.                     sqlCommand.Connection = connexion;
  59.                     // exécution requête
  60.                     int nbLignes = sqlCommand.ExecuteNonQuery();
  61.                     // affichage résultat
  62.                     Console.WriteLine("Il y a eu {0} ligne(s) modifiée(s)", nbLignes);
  63.                 }
  64.             } catch (Exception ex) {
  65.                 // msg d'erreur
  66.                 Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
  67.             }
  68.         }
  69.
  70.         // exécution d'une requête Select
  71.         static void ExecuteSelect(DbProviderFactory connecteur, string connectionString, string requête) {
  72.             // on gère les éventuelles exceptions
  73.             try {
  74.                 using (DbConnection connexion = connecteur.CreateConnection()) {
  75.                     // configuration connexion
  76.                     connexion.ConnectionString = connectionString;
  77.                     // ouverture connexion
  78.                     connexion.Open();
  79.                     // configuration Command
  80.                     DbCommand sqlCommand = connecteur.CreateCommand();
  81.                     sqlCommand.CommandText = requête;
  82.                     sqlCommand.Connection = connexion;
  83.                     // exécution requête
  84.                     DbDataReader reader = sqlCommand.ExecuteReader();
  85.                     // affichage des résultats
  86. ...
  87.                 }
  88.             } catch (Exception ex) {
  89.                 // msg d'erreur
  90.                 Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
  91.             }
  92.         }
  93.     }
  94. }
  • lignes 12-14 : l'application reçoit deux paramètres : le nom du connecteur générique ainsi que la chaîne de connexion à la base de données sous la forme de clés du fichier [App.config].
  • lignes 23, 25 : on récupère dans [App.config], le nom du connecteur générique ainsi que la chaîne de connexion
  • ligne 27 : le connecteur générique est instancié. A partir de ce moment, il est associé à un SGBD particulier.
  • lignes 39-43 : l'exécution de l'ordre SQL saisi au clavier est déléguée à deux méthodes auxquelles on passe :
  • la requête à exécuter
  • la chaîne de connexion qui identifie la base sur laquelle la requête sera exécutée
  • le connecteur générique qui identifie les classes à utiliser pour dialoguer avec le SGBD gérant la base.
  • lignes 50-54 : une connexion est obtenue avec la méthode CreateConnection (ligne 50) du connecteur générique puis configurée avec la chaîne de connexion de la base à gérer (ligne 52). Elle est ensuite ouverte (ligne 54).
  • lignes 56-58 : l'objet Command nécessaire à l'exécution de l'ordre SQL est créé avec la méthode CreateCommand du connecteur générique. Il est ensuite configuré avec le texte de l'ordre SQL à exécuter (ligne 57) et la connexion sur laquelle exécuter celui-ci (ligne 58).
  • ligne 60 : l'ordre SQL de mise à jour est exécuté
  • lignes 74-87 : on trouve un code analogue. La nouveauté se trouve ligne 84. L'objet Reader obtenu par l'exécution de l'ordre Select est de type DbDataReader qui s'utilise comme les objets OleDbDataReader , OdbcDataReader… que nous avons déjà rencontrés.

Voici quelques exemples d'exécution.

Avec la base MySQL5 :

Image non disponible

On ouvre la page de propriétés du projet [1] et on sélectionne l'onglet [Debug] [2]. En [3], la clé du connecteur de la ligne 14 de [App.config]. En [4], la clé de la chaîne de connexion de la ligne 6 de [App.config]. Les résultats de l'exécution sont les suivants :

Cacher Sélectionnez
  1. Provider factory : [MySql.Data.MySqlClient]
  2. Chaîne de connexion à la base : [Server=localhost;Database=dbarticles;Uid=root;Pwd=root;]
  3.
  4. Requête SQL (rien pour arrêter) : select * from articles
  5.
  6. ------------------------------------
  7. ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
  8. ------------------------------------
  9.
  10. 1 article1 100 10 1
  11. 2 article2 200 20 2
  12. 3 article3 300 30 3

Avec la base SQL Server Compact :

Image non disponible

En [1], la clé du connecteur de la ligne 13 de [App.config]. En [2], la clé de la chaîne de connexion de la ligne 4 de [App.config]. Les résultats de l'exécution sont les suivants :

Cacher Sélectionnez
  1. Provider factory : [System.Data.SqlServerCe.3.5]
  2. Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]
  3.
  4. Requête SQL (rien pour arrêter) : select * from articles
  5.
  6. ------------------------------------
  7. ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
  8. ------------------------------------
  9.
  10. 1 vélo 500 10 5
  11. 2 pompe 10 10 2
  12. 3 arc 600 4 1
  13. 4 flèches - lot de 6 100 12 20
  14. 5 combinaison de plongée 300 8 2
  15. 6 bouteilles d'oxygène 120 10 5

Le lecteur est invité à tester les autres bases de données.

X-D-6. Quel connecteur choisir ?

Revenons à l'architecture d'une application avec bases de données :

Image non disponible

Nous avons vu divers types de connecteurs ADO.NET :

  • les connecteurs propriétaires sont les plus performants mais rendent la couche [dao] dépendante de classes propriétaires. Changer le SGBD implique de changer la couche [dao].
  • les connecteurs ODBC ou OLE DB permettent de travailler avec de multiples bases de données sans changer la couche [dao]. Ils sont moins performants que les connecteurs propriétaires.
  • le connecteur générique s'appuie sur les connecteurs propriétaires tout en présentant une interface standard à la couche [dao].

Il semble donc que le connecteur générique soit le connecteur idéal. Dans la pratique, le connecteur générique n'arrive cependant pas à cacher toutes les particularités d'un SGBD derrière une interface standard. Nous allons voir dans le paragraphe suivant, la notion de requête paramétrée. Avec SQL Server, une requête paramétrée à la forme suivante :

Cacher Sélectionnez
  1. insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)

Avec MySQL5, la même requête s'écrirait :

Cacher Sélectionnez
  1. insert into articles(nom,prix,stockactuel,stockminimum) values(?,?,?,?)

Il y a donc une différence de syntaxe. La propriété de l'interface IDbCommand décrite page , liée aux paramètres est la suivante :

Parameters la liste des paramètres d'un ordre SQL paramétré. L'ordre update articles set prix=prix*1.1 where id=@id a le paramètre @id.

La propriété Parameters est de type IDataParameterCollection , une interface. Elle représente l'ensemble des paramètres de l'ordre SQL CommandText . La propriété Parameters a une méthode Add pour ajouter des paramètres de type IDataParameter , de nouveau une interface. Celle-ci a les propriétés suivantes :

  • ParameterName : nom du paramètre
  • DbType : le type SQL du paramètre
  • Value : la valeur affectée au paramètre
  • …

Le type IDataParameter convient bien aux paramètres de l'ordre SQL

Cacher Sélectionnez
  1. insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)

car on y trouve des paramètres nommés. La propriété ParameterName peut être utilisée.

Le type IDataParameter ne convient pas à l'ordre SQL

Cacher Sélectionnez
  1. insert into articles(nom,prix,stockactuel,stockminimum) values(?,?,?,?)

car les paramètres ne sont pas nommés. C'et l'ordre d'ajout des paramètres dans la collection [IDbCommand.Parameters] qui est alors pris en compte. Dans cet exemple, il faudra insérer les 4 paramètres dans l'ordre nom , prix , stockactuel , stockminimum . Dans la requête avec paramètres nommés, l'ordre d'ajout des paramètres n'a pas d'importance. Au final, le développeur ne peut faire totalement abstraction du SGBD qu'il utilise lorsqu'il initialise les paramètres d'une requête paramétrée. On a là une des limites actuelles du connecteur générique.

Il existe des frameworks qui s'affranchissent de ces limites et qui apportent par ailleurs de nouvelles fonctionnalités à la couche [dao] :

Image non disponible

Un framework est un ensemble de bibliothèques de classes visant à faciliter une certaine façon d'architecturer l'application. Il en existe plusieurs qui permettent l'écriture de couches [dao] à la fois performantes et insensibles au changement de SGBD :

  • Spring.Net [http://www.springframework.net/] déjà présenté dans ce document offre l'équivalent du connecteur générique étudié, sans ses limitations, ainsi que des facilités diverses qui simplifient l'accès aux données. Il existe une version Java.
  • iBatis.Net [http://ibatis.apache.org] est plus ancien et plus riche que Spring.Net. Il existe une version Java.
  • NHibernate [http://www.hibernate.org/] est un portage de la version Java Hibernate très connue dans le monde Java. NHibernate permet à la couche [dao] d'échanger avec le SGBD sans émettre d'ordres SQL. La couche [dao] travaille avec des objets Hibernate. Un langage de requêtes HBL (Hibernate Query language) permet de requêter les objets gérés par Hibernate. Ces sont ces derniers qui émettent les ordres SQL. Hibernate sait s'adapter aux SQL propriétaires des SGBD.
  • LINQ (Language INtegrated Query), intégrée à la version 3.5 .NET et disponible dans C# 2008. LINQ marche sur les pas de NHibernate , mais pour l'instant (mai 2008) seul le SGBD SQL Server est supporté. Ceci devrait évoluer avec le temps. LINQ va plus loin que NHibernate : son langage de requêtes permet d'interroger de façon standard trois types différents de sources de données :
  • des collections d'objets (LINQ to Objects)
  • un fichier Xml (LINQ to Xml)
  • une base de données (LINQ to SQL)

Ces frameworks ne seront pas abordés dans ce document. Il est cependant vivement conseillé de les utiliser dans les applications professionnelles.

X-E. Requêtes paramétrées

Nous avons évoqué dans le paragraphe précédent les requêtes paramétrées. Nous les présentons ici avec un exemple pour le SGBD SQL Server Compact. Le projet est le suivant

Image non disponible
  • en [1], le projet. Seuls [App.config], [Article.cs] et [Parametres.cs] sont utilisés. On notera également la base SQL Server Ce [dbarticles.sdf].
  • en [2], le projet est configuré pour exécuter [Parametres.cs]
  • en [3], les références du projet

Le fichier de configuration [App.config] définit la chaîne de connexion à la base de données :

Cacher Sélectionnez
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.     <connectionStrings>
  4.         <add name="dbArticlesSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
  5.     </connectionStrings>
  6. </configuration>

Le fichier [Article.cs] définit une classe [Article]. Un objet Article sera utilisé pour encapsuler les informations d'une ligne de la table ARTICLES de la base de données [dbarticles.sdf] :

Cacher Sélectionnez
  1. namespace Chap7 {
  2.     class Article {
  3.         // propriétés
  4.         public int Id { get; set; }
  5.         public string Nom { get; set; }
  6.         public decimal Prix { get; set; }
  7.         public int StockActuel { get; set; }
  8.         public int StockMinimum { get; set; }
  9.
  10.         // constructeurs
  11.         public Article() {
  12.         }
  13.
  14.         public Article(int id, string nom, decimal prix, int stockActuel, int stockMinimum) {
  15.             Id = id;
  16.             Nom = nom;
  17.             Prix = prix;
  18.             StockActuel = stockActuel;
  19.             StockMinimum = stockMinimum;
  20.         }
  21.
  22.     }
  23. }

L'application [Parametres.cs] met en œuvre les requêtes paramétrées :

Cacher Sélectionnez
  1. using System;
  2. using System.Data.SqlServerCe;
  3. using System.Text;
  4. using System.Data;
  5. using System.Configuration;
  6.
  7. namespace Chap7 {
  8.     class Parametres {
  9.         static void Main(string[] args) {
  10.
  11.             // exploitation du fichier de configuration
  12.             string connectionString = null;
  13.             try {
  14.                 // chaîne de connexion
  15.                 connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString;
  16.             } catch (Exception e) {
  17.                 Console.WriteLine("Erreur de configuration : {0}", e.Message);
  18.                 return;
  19.             }
  20.
  21.             // affichages
  22.             Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);
  23.
  24.             // création d'un tableau d'articles
  25.             Article[] articles = new Article[5];
  26.             for (int i = 1; i <= articles.Length; i++) {
  27.                 articles[i-1] = new Article(0, "article" + i, i * 100, i * 10, i);
  28.             }
  29.
  30.             // on gère les éventuelles exceptions
  31.             try {
  32.
  33.                 // on supprime les articles existants de la base
  34.                 ExecuteUpdate(connectionString, "delete from articles");
  35.
  36.                 // on affiche les articles de la table
  37.                 ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
  38.
  39.                 // on insère le tableau des articles dans la base
  40.                 InsertArticles(connectionString, articles);
  41.
  42.                 // on affiche les articles de la table
  43.                 ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
  44.             } catch (Exception ex) {
  45.                 // msg d'erreur
  46.                 Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
  47.             }
  48.         }
  49.
  50.         // insertion d'un tableau d'articles
  51.         static void InsertArticles(string connectionString, Article[] articles) {
  52.             using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
  53.                 // ouverture connexion
  54.                 connexion.Open();
  55.                 // configuration commande
  56.                 string requête = "insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)";
  57.                 SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
  58.                 sqlCommand.Parameters.Add("@nom",SqlDbType.NVarChar,30);
  59.                 sqlCommand.Parameters.Add("@prix", SqlDbType.Money);
  60.                 sqlCommand.Parameters.Add("@sa", SqlDbType.Int);
  61.                 sqlCommand.Parameters.Add("@sm", SqlDbType.Int);
  62.                 // compilation de la commande
  63.                 sqlCommand.Prepare();
  64.                 // insertion des lignes
  65.                 for (int i = 0; i < articles.Length; i++) {
  66.                     // initialisation paramètres
  67.                     sqlCommand.Parameters["@nom"].Value = articles[i].Nom;
  68.                     sqlCommand.Parameters["@prix"].Value = articles[i].Prix;
  69.                     sqlCommand.Parameters["@sa"].Value = articles[i].StockActuel;
  70.                     sqlCommand.Parameters["@sm"].Value = articles[i].StockMinimum;
  71.                     // exécution requête
  72.                     sqlCommand.ExecuteNonQuery();
  73.                 }
  74.             }
  75.         }
  76.
  77.         // exécution d'une requête de mise à jour
  78.         static void ExecuteUpdate(string connectionString, string requête) {
  79. ...
  80.         }
  81.
  82.         // exécution d'une requête Select
  83.         static void ExecuteSelect(string connectionString, string requête) {
  84. ...
  85.         }
  86.
  87.         // affichage reader
  88.         static void AfficheReader(IDataReader reader) {
  89. ...
  90.     }
  91. }

La nouveauté par rapport à ce qui a été vu précédemment est la procédure [InsertArticles] des lignes 51-75 :

  • ligne 51 : la procédure reçoit deux paramètres :
  • la chaîne de connexion connectionString qui va permettre à la procédure de se connecter à la base
  • un tableau d'objets Article qu'il faut ajouter à la table Articles de la base de données
  • ligne 56 : la requête d'insertion d'un objet [Article]. Elle a quatre paramètres :
  • @ nom : le nom de l'article
  • @ prix : son prix
  • @ sa : son stock actuel
  • @ sm : son stock minimum

La syntaxe de cette requête paramétrée est propriétaire à SQL Server Compact. Nous avons vu dans le paragraphe précédent qu'avec MySQL5, la syntaxe serait la suivante :

Cacher Sélectionnez
  1. insert into articles(nom,prix,stockactuel,stockminimum) values(?,?,?,?)

Avec SQL Server Compact, chaque paramètre doit être précédé du caractère @. Le nom des paramètres est libre.

  • lignes 58-61 : on définit les caractéristiques de chacun des 4 paramètres et on les ajoute, un par un, à la liste des paramètres de l'objet SqlCeCommand qui encapsule l'ordre SQL qui va être exécuté.

On utilise ici la méthode [SqlCeCommand].Parameters.Add qui possède six signatures. Nous utilisons les deux suivantes :

Add(string parameterName, SQLDbType type)

ajoute et configure le paramètre nommé parameterName . Ce nom doit être l'un de ceux de la requête paramétrée configurée : (@nom…). type désigne le type SQL de la colonne concernée par le paramètre. On dispose de nombreux types dont les suivants :

type SQL | type C# | commentaire
BigInt | Int64 |
DateTime | DateTime |
Decimal | Decimal |
Float | Double |
Int | Int32 |
Money | Decimal |
NChar | String | chaîne de longueur fixe
NVarChar | String | chaîne de longueur variable
Real | Single |

Add(string parameterName, SQLDbType type, int size)

le troisième paramètre size fixe la taille de la colonne. Cette information n'est utile que pour certains types SQL, le type NVarChar par exemple.

  • ligne 63 : on compile la requête paramétrée. On dit aussi qu'on la prépare, d'où le nom de la méthode. Cette opération n'est pas indispensable. Elle est là pour améliorer les performances. Lorsqu'un SGBD exécute un ordre SQL, il fait un certain travail d'optimisation avant de l'exécuter. Une requête paramétrée est destinée à être exécutée plusieurs fois avec des paramètres différents. Le texte de la requête lui ne change pas. Le travail d'optimisation peut alors n'être fait qu'une fois. Certains SGBD ont la possibilité de "préparer" ou "compiler" des requêtes paramétrées. Un plan d'exécution est alors défini pour cette requête. C'est la phase d'optimisation dont on a parlé. Une fois compilée, la requête est exécutée de façon répétée avec à chaque fois de nouveaux paramètres effectifs mais le même plan d'exécution.

La compilation n'est pas l'unique avantage des requêtes paramétrées. Reprenons la requête étudiée :

Cacher Sélectionnez
  1. insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)

On pourrait vouloir construire le texte de la requête par programme :

Cacher Sélectionnez
  1. string requête="insert into articles(nom,prix,stockactuel,stockminimum) values('"+nom+"',"+prix+","+sa+","+sm+")";

Ci-dessus si (nom,prix,sa,sm) vaut ("article1",100,10,1), la requête précédente devient :

Cacher Sélectionnez
  1. string requête="insert into articles(nom,prix,stockactuel,stockminimum) values('article1',100,10,1)";

Maintenant si (nom,prix,sa,sm) vaut ("l'article1",100,10,1), la requête précédente devient :

Cacher Sélectionnez
  1. string requête="insert into articles(nom,prix,stockactuel,stockminimum) values('l'article1',100,10,1)";

et devient syntaxiquement incorrecte à cause de l'apostrophe du nom l'article1 . Si nom provient d'une saisie de l'utilisateur, cela veut dire que nous sommes amenés à vérifier si la saisie n'a pas d'apostrophes et si elle en a, à les neutraliser. Cette neutralisation est dépendante du SGBD. L'intérêt de la requête préparée est qu'elle fait elle-même ce travail. Cette facilité justifie à elle seule l'utilisation d'une requête préparée.

  • lignes 65-73 : les articles du tableau sont insérés un à un
  • lignes 67-70 : chacun des quatre paramètres de la requête reçoit sa valeur via sa propriété Value .
  • ligne 72 : la requête d'insertion maintenant complète est exécutée de la façon habituelle.

Voici un exemple d'exécution :

Cacher Sélectionnez
  1. Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]
  2.
  3. Il y a eu 5 ligne(s) modifiée(s)
  4.
  5. ------------------------------------
  6. ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
  7. ------------------------------------
  8.
  9.
  10. ------------------------------------
  11. ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
  12. ------------------------------------
  13.
  14. 117 article1 100 10 1
  15. 118 article2 200 20 2
  16. 119 article3 300 30 3
  17. 120 article4 400 40 4
  18. 121 article5 500 50 5
  • ligne 3 : message après la suppression de toutes les lignes de la table
  • lignes 5-7 : montrent que la table est vide
  • lignes 10-18 : montrent la table après l'insertion des 5 articles

X-F. Transactions

X-F-1. Généralités

Une transaction est une suite d'ordres SQL exécutée de façon "atomique" :

  • soit toutes les opérations réussissent
  • soit l'une d'elles échoue et alors toutes celles qui ont précédé sont annulées

Au final, les opérations d'une transaction ont soit toutes été appliquées avec succès, soit aucune n'a été appliquée. Lorsque l'utilisateur a lui-même la maîtrise de la transaction, il valide une transaction par un ordre COMMIT ou l'annule par un ordre ROLLBACK.

Dans nos exemples précédents, nous n'avons pas utilisé de transaction. Et pourtant il y en avait, car dans un SGBD un ordre SQL s'exécute toujours au sein d'une transaction. Si le client .NET ne démarre pas lui-même une transaction explicite, le SGBD utilise une transaction implicite. Il y a alors deux cas courants :

  • chaque ordre SQL individuel fait l'objet d'une transaction, initiée par le SGBD avant l'ordre et fermée ensuite. On dit qu'on est en mode autocommit. Tout se passe donc comme si le client .NET faisait des transactions pour chaque ordre SQL.
  • le SGBD n'est pas en mode autocommit et commence une transaction implicite au 1er ordre SQL que le client .NET émet en dehors d'une transaction et il laisse le client la fermer. Tous les ordres SQL émis par le client .NET font alors partie de la transaction implicite. Celle-ci peut se terminer sur différents événements : le client ferme la connexion, commence une nouvelle transaction… mais on est alors dans une situation dépendante du SGBD. Cest un mode à éviter.

Le mode par défaut est généralement fixé par configuration du SGBD. Certains SGBD sont par défaut en mode autocommit, d'autres pas. SQLServer Compact est par défaut en mode autocommit.

Les ordres SQL des différents utilisateurs s'exécutent en même temps dans des transactions qui travaillent en parallèle. Les opérations faites par une transaction peuvent affecter celles faites par une autre transaction. On distingue quatre niveaux d'étanchéité entre les transactions des différents utilisateurs :

  • Uncommitted Read
  • Committed Read
  • Repeatable Read
  • Serializable

Uncommitted Read

Ce mode d'isolation est également appelé "Dirty Read". Voici un exemple de ce qui se peut se passer dans ce mode :

  • un utilisateur U1 commence une transaction sur une table T
  • un utilisateur U2 commence une transaction sur cette même table T
  • l'utilisateur U1 modifie des lignes de la table T mais ne les valide pas encore
  • l'utilisateur U2 "voit" ces modifications et prend des décisions à partir de ce qu'il voit
  • l'utilisateur annule sa transaction par un ROLLBACK

On voit qu'en 4, l'utilisateur U2 a pris une décision à partir de données qui s'avèreront fausses ultérieurement.

Committed Read

Ce mode d'isolation évite l'écueil précédent. Dans ce mode, l'utilisateur U2 à l'étape 4 ne "verra" pas les modifications apportées par l'utilisateur U1 à la table T. Il ne les verra qu'après que U1 ait fait un COMMIT de sa transaction.

Dans ce mode, également appelé "Unrepeatable Read", on peut néanmoins rencontrer les situations suivantes :

  • un utilisateur U1 commence une transaction sur une table T
  • un utilisateur U2 commence une transaction sur cette même table T
  • l'utilisateur U2 fait un SELECT pour obtenir la moyenne d'une colonne C des lignes de T vérifiant une certaine condition
  • l'utilisateur U1 modifie (UPDATE) certaines valeurs de la colonne C de T et les valide (COMMIT)
  • l'utilisateur U2 refait le même SELECT qu'en 3. Il découvrira que la moyenne de la colonne C a changé à cause des modifications faites par U1.

Maintenant l'utilisateur U2 ne voit que les modifications "validées" par U1. Mais alors qu'il reste dans la même transaction, deux opérations identiques 3 et 5 donnent des résultats différents. Le terme "Unrepeatable Read" désigne cette situation. C'est une situation ennuyeuse pour quelqu'un qui désire avoir une image stable de la table T.

Repeatable Read

Dans ce mode d'isolation, un utilisateur est assuré d'avoir les mêmes résultats pour ses lectures de la base tant qu'il reste dans la même transaction. Il travaille sur une photo sur laquelle ne sont jamais répercutées les modifications apportées par les autres transactions, mêmes validées. Il ne verra celles-ci que lorsque lui-même terminera sa transaction par un COMMIT ou ROLLBACK.

Ce mode d'isolation n'est cependant pas encore parfait. Après l'opération 3 ci-dessus, les lignes consultées par l'utilisateur U2 sont verrouillées. Lors de l'opération 4, l'utilisateur U1 ne pourra pas modifier (UPDATE) les valeurs de la colonne C de ces lignes. Il peut cependant rajouter des lignes (INSERT). Si certaines des lignes ajoutées vérifient la condition testée en 3, l'opération 5 donnera une moyenne différente de celle trouvée en 3 à cause des lignes rajoutées. On appelle parfois ces lignes des lignes fantômes.

Pour résoudre ce nouveau problème, il faut passer en isolation "Serializable".

Serializable

Dans ce mode d'isolation, les transactions sont complètement étanches les unes des autres. Il assure que le résultat de deux transactions menées simultanément donneront le même résultat que si elles étaient faites l'une après l'autre. Pour arriver à ce résultat, lors de l'opération 4 où l'utilisateur U1 veut ajouter des lignes qui changeraient le résultat du SELECT de l'utilisateur U1, il en sera empêché. Un message d'erreur lui indiquera que l'insertion n'est pas possible. Elle le deviendra lorsque l'utilisateur U2 aura validé sa transaction.

Les quatres niveaux SQL d'isolation des transactions ne sont pas disponibles dans tous les SGBD. Le niveau d'étanchéité par défaut est en général le niveau Committed Read. Le niveau d'étanchéité désiré pour une transaction peut être indiqué explicitement lors de la création d'une transaction explicite par un client .NET.

X-F-2. L'API de gestion des transactions

Une connexion implémente l'interface IDbConnection présentée page . Cette interface à la méthode suivante :

BeginTransaction M démarre une transaction.

Cette méthode a deux signatures :

  • IDbTransaction BeginTransaction() : démarre une transaction et rend l'objet IDbTransaction permettant de la contrôler
  • IDbTransaction BeginTransaction(IsolationLevel level) : précise de plus le niveau d'étanchéité désiré pour la transaction. level prend ses valeurs dans l'énumération suivante :
ReadUncommitted | la transaction peut lire des données écrites par une autre transaction que celle-ci n'a pas encore validées - à éviter
ReadCommitted | la transaction ne peut pas lire des données écrites par une autre transaction que celle-ci n'a pas encore validées. Les données lues deux fois de suite dans la transaction peuvent cependant changer (not repeatable reads) car une autre transaction a pu les modifier entre-temps (les lignes lues ne sont pas verrouillées - seules les lignes mises à jour le sont). Par ailleurs, une autre transaction a pu ajouter des lignes (lignes fantômes) qui seront intégrées dans la seconde lecture.
RepeatableRead | les lignes lues par la transaction sont verrouillées à l'instar des lignes mises à jour. Cela empêche une autre transaction de les modifier. Cela n'évite pas l'ajout de lignes.
Serializable | les tables exploitées par la transaction sont verrouillées empêchant l'ajout de nouvelles lignes par une autre transaction. Tout se passe comme si la transaction était seule. Diminue les performances car les transactions ne travaillent plus en parallèle.
Snapshot | la transaction travaille sur une copie des données faite au temps T. Utilisée lorsque la transaction est en lecture seule. Donne le même résultat que serializable en évitant son coût.

Une fois la transaction démarrée, elle est contrôlée par l'objet de type IDbTransaction , une interface dont nous utiliserons les propriétés P et méthodes M suivantes :

Nom | Type | Rôle
Connection | P | la connexion IDbConnection qui supporte la transaction
Commit | M | valide la transaction - les résultats des ordres SQL émis dans la transaction sont copiés dans la base.
Rollback | M | invalide la transaction - les résultats des ordres SQL émis dans la transaction ne sont pas copiés dans la base.

X-F-3. Le programme exemple

Nous reprenons le projet précédent pour nous intéresser maintenant au programme [Transactions.cs] :

Image non disponible
  • en [1], le projet.
  • en [2], le projet est configuré pour exécuter [Transactions.cs]

Le code de [Transactions.cs] est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Configuration;
  3. using System.Data;
  4. using System.Data.SqlServerCe;
  5. using System.Text;
  6.
  7. namespace Chap7 {
  8.     class Transactions {
  9.         static void Main(string[] args) {
  10.
  11.             // exploitation du fichier de configuration
  12.             string connectionString = null;
  13.             try {
  14.                 // chaîne de connexion
  15.                 connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString;
  16.             } catch (Exception e) {
  17.                 Console.WriteLine("Erreur de configuration : {0}", e.Message);
  18.                 return;
  19.             }
  20.
  21.             // affichages
  22.             Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);
  23.
  24.             // création d'un tableau de 2 articles de même nom
  25.             Article[] articles = new Article[2];
  26.             for (int i = 1; i <= articles.Length; i++) {
  27.                 articles[i - 1] = new Article(0, "article", i * 100, i * 10, i);
  28.             }
  29.             // on gère les éventuelles exceptions
  30.             try {
  31.                 Console.WriteLine("Insertion sans transaction...");
  32.                 // on insère le tableau des articles dans la base d'abord sans transaction
  33.                 ExecuteUpdate(connectionString, "delete from articles");
  34.                 try {
  35.                     InsertArticlesOutOfTransaction(connectionString, articles);
  36.                 } catch (Exception ex) {
  37.                     // msg d'erreur
  38.                     Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
  39.                 }
  40.                 ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
  41.
  42.                 // on refait la même chose mais dans une transaction cette fois
  43.                 Console.WriteLine("\n\nInsertion dans une transaction...");
  44.                 ExecuteUpdate(connectionString, "delete from articles");
  45.                 InsertArticlesInTransaction(connectionString, articles);
  46.                 ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
  47.             } catch (Exception ex) {
  48.                 // msg d'erreur
  49.                 Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
  50.             }
  51.         }
  52.
  53.         // insertion d'un tableau d'articles sans transaction
  54.         static void InsertArticlesOutOfTransaction(string connectionString, Article[] articles) {
  55. ....
  56.         }
  57.
  58.         // insertion d'un tableau d'articles dans une transaction
  59.         static void InsertArticlesInTransaction(string connectionString, Article[] articles) {
  60. ....
  61.         }
  62.
  63.         // exécution d'une requête de mise à jour
  64.         static void ExecuteUpdate(string connectionString, string requête) {
  65. ....
  66.         }
  67.
  68.         // exécution d'une requête Select
  69.         static void ExecuteSelect(string connectionString, string requête) {
  70. ...
  71.         }
  72.
  73.         // affichage reader
  74.         static void AfficheReader(IDataReader reader) {
  75. ...
  76.             }
  77.         }
  78.     }
  79. }
  • lignes 12-19 : la chaîne de connexion à la base SQLServer Ce est lue dans [App.config]
  • lignes 25-28 : un tableau de deux objets Article est créé. Ces deux articles ont le même nom "article". Or, la base [dbarticles.sdf] a une contrainte d'unicité sur sa colonne [nom] (cf page ). Donc ces deux articles ne peuvent être présents en même temps dans la base. Les deux articles de nom "article" sont ajoutés dans la table articles . Il va donc y avoir un problème, c.a.d. une exception lancée par le SGBD et relayée par son connecteur ADO.NET. Pour montrer l'effet de la transaction, les deux articles vont être insérés dans deux environnements différents :
  • d'abord en-dehors de toute transaction. Il faut se rappeler ici que, dans ce cas, SQLServer Compact travaille en mode autocommit, c.a.d. insère chaque ordre SQL dans une transaction implicite. Le 1er article va être inséré. Le second ne le sera pas.
  • ensuite dans une transaction explicite encapsulant les deux insertions. Parce que la deuxième insertion va échouer, la première sera alors défaite. Au final aucune insertion ne sera faite.
  • ligne 33 : la table articles est vidée
  • ligne 35 : l'insertion des deux articles sans transaction explicite. Parce qu'on sait que la deuxième insertion va provoquer une exception, celle-ci est gérée par un try / catch
  • ligne 46 : affichage de la table articles
  • lignes 44-46 : on refait la même séquence mais cette fois ci une transaction explicite est utilisée pour faire les insertions. L'exception qui est rencontrée est ici gérée par la méthode InsertArticlesInTransaction .
  • lignes 54-56 : la méthode InsertArticlesOutOfTransaction est la méthode InsertArticles du programme [Parametres.cs] étudié précédemment.
  • lignes 64-66 : la méthode ExecuteUpdate est la même que précédemment. L'ordre SQL exécuté l'est dans une transaction implicite. C'est possible ici car on sait que dans ce cas, SQLServer Compact travaille en mode autocommit.
  • lignes 69-71 : idem pour la méthode ExecuteSelect .

La méthode InsertArticlesInTransaction est la suivante :

Cacher Sélectionnez
  1. // insertion d'un tableau d'articles dans une transaction
  2.         static void InsertArticlesInTransaction(string connectionString, Article[] articles) {
  3.             using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
  4.                 // ouverture connexion
  5.                 connexion.Open();
  6.                 // configuration commande
  7.                 string requête = "insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)";
  8.                 SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
  9.                 sqlCommand.Parameters.Add("@nom", SqlDbType.NVarChar, 30);
  10.                 sqlCommand.Parameters.Add("@prix", SqlDbType.Money);
  11.                 sqlCommand.Parameters.Add("@sa", SqlDbType.Int);
  12.                 sqlCommand.Parameters.Add("@sm", SqlDbType.Int);
  13.                 // compilation de la commande
  14.                 sqlCommand.Prepare();
  15.                 // transaction
  16.                 SqlCeTransaction transaction = null;
  17.                 try {
  18.                     // début transaction
  19.                     transaction = connexion.BeginTransaction(IsolationLevel.ReadCommitted);
  20.                     // la commande SQL doit être exécutée dans cette transaction
  21.                     sqlCommand.Transaction = transaction;
  22.                     // insertion des lignes
  23.                     for (int i = 0; i < articles.Length; i++) {
  24.                         // initialisation paramètres
  25.                         sqlCommand.Parameters["@nom"].Value = articles[i].Nom;
  26.                         sqlCommand.Parameters["@prix"].Value = articles[i].Prix;
  27.                         sqlCommand.Parameters["@sa"].Value = articles[i].StockActuel;
  28.                         sqlCommand.Parameters["@sm"].Value = articles[i].StockMinimum;
  29.                         // exécution requête
  30.                         sqlCommand.ExecuteNonQuery();
  31.                     }
  32.                     // on valide la transaction
  33.                     transaction.Commit();
  34.                     Console.WriteLine("transaction validée...");
  35.                 } catch {
  36.                     // on défait la transaction
  37.                     if (transaction != null)transaction.Rollback();
  38.                     Console.WriteLine("transaction invalidée...");
  39.                 }
  40.             }
  41.         }

Nous ne détaillons que ce qui la différencie de la méthode InsertArticles du programme [Parametres.cs] étudié précédemment :

  • ligne 16 : une transaction SqlCeTransaction est déclarée.
  • lignes 17, 35 : le try / catch pour gérer l'exception qui va surgir à l'issue de la 2ième insertion
  • ligne 19 : la transaction est créée. Elle appartient à la connexion courante.
  • ligne 21 : la commande SQL paramétrée est mise dans la transaction
  • lignes 23-31 : les insertions sont faites
  • ligne 33 : tout s'est bien passé - la transaction est validée - les insertions vont être définitivement intégrées à la base de données.
  • ligne 37 : on a eu un problème. La transaction est défaite si elle existait.

L'exécution donne les résultats suivants :

Cacher Sélectionnez
  1. Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]
  2.
  3. Insertion sans transaction...
  4. Il y a eu 0 ligne(s) modifiée(s)
  5. Erreur d'accès à la base de données (A duplicate value cannot be inserted into a unique index. [ Table name = ARTICLES,Constraint name = UQ__ARTICLES__0000000000000010 ])
  6.
  7. ------------------------------------
  8. ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
  9. ------------------------------------
  10.
  11. 126 article 100 10 1
  12.
  13.
  14. Insertion dans une transaction...
  15. Il y a eu 1 ligne(s) modifiée(s)
  16. transaction invalidée...
  17.
  18. ------------------------------------
  19. ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
  20. ------------------------------------
  • ligne 4 : affichée par le ExecuteUpdate ("delete from articles") - il n'y avait pas de lignes dans la table
  • ligne 5 : l'exception provoquée par la deuxième insertion. Le message indique que la contrainte UQ__ARTICLES__0000000000000010 n'a pas été vérifiée. On peut en savoir plus en regardant les propriétés de la base :
Image non disponible
  • en [1] dans la vue [Database Explorer] de Visual Studio, on a créé une connexion [2] sur la base [dbarticles.sdf]. Celle-ci a un index UQ__ARTICLES__0000000000000010 . En cliquant droit sur cet index, on a accès à ses propriétés (Index properties)
  • en [3,4], on voit que l' index UQ__ARTICLES__0000000000000010 corespond à une contrainte d'unicité sur la colonne [NOM]
  • lignes 7-11 : affichage de la table articles après les deux insertions. Elle n'est pas vide : le 1er article a été inséré.
  • ligne 15 : affichée par le ExecuteUpdate ("delete from articles") - il y avait une ligne dans la table
  • ligne 16 : message affiché par InsertArticlesInTransaction lorsque la transaction échoue.
  • lignes 18-20 : montrent qu'aucune insertion n'a été faite. Le Rollback de la transaction a défait la 1re insertion.

X-G. La méthode ExecuteScalar

Parmi les méthodes de l'interface IDbCommand décrite page , il y avait la méthode suivante :

ExecuteScalar M pour exécuter un ordre SQL Select ne rendant qu'un unique résultat comme dans : select count(*) from articles .

Nous montrons ici un exemple d'utilisation de cette méthode. Revenons au projet :

Image non disponible
  • en [1], le projet.
  • en [2], le projet est configuré pour exécuter [ExecuteScalar.cs]

Le programme [ExecuteScalar.cs] est le suivant :

Cacher Sélectionnez
  1. ...
  2. namespace Chap7 {
  3.     class Scalar {
  4.         static void Main(string[] args) {
  5.
  6.             // exploitation du fichier de configuration
  7.             string connectionString = null;
  8. ...
  9.
  10.             // affichages
  11.             Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);
  12.
  13.             // création d'un tableau de 5 articles
  14.             Article[] articles = new Article[5];
  15.             for (int i = 1; i <= articles.Length; i++) {
  16.                 articles[i - 1] = new Article(0, "article" + i, i * 100, i * 10, i);
  17.             }
  18.
  19.             // on gère les éventuelles exceptions
  20.             try {
  21.                 // on insère le tableau des articles dans une transaction
  22.                 ExecuteUpdate(connectionString, "delete from articles");
  23.                 InsertArticlesInTransaction(connectionString, articles);
  24.                 ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
  25.                 // on calcule la moyenne des prix des articles
  26.                 decimal prixMoyen = (decimal)ExecuteScalar(connectionString, "select avg(prix) from articles");
  27.                 Console.WriteLine("Prix moyen des articles={0}", prixMoyen);
  28.                 // ou le nombre des articles
  29.                 int nbArticles = (int)ExecuteScalar(connectionString, "select count(id) from articles");
  30.                 Console.WriteLine("Nombre d'articles={0}", nbArticles);
  31.             } catch (Exception ex) {
  32.                 // msg d'erreur
  33.                 Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
  34.             }
  35.         }
  36.
  37.         // insertion d'un tableau d'articles dans une transaction
  38.         static void InsertArticlesInTransaction(string connectionString, Article[] articles) {
  39. ...
  40.         }
  41.
  42.
  43.         // exécution d'une requête de mise à jour
  44.         static object ExecuteScalar(string connectionString, string requête) {
  45.             using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
  46.                 // ouverture connexion
  47.                 connexion.Open();
  48.                 // exécution requête
  49.                 return new SqlCeCommand(requête, connexion).ExecuteScalar();
  50.             }
  51.         }
  52.
  53.         // exécution d'une requête de mise à jour
  54.         static void ExecuteUpdate(string connectionString, string requête) {
  55. ...
  56.         }
  57.
  58.         // exécution d'une requête Select
  59.         static void ExecuteSelect(string connectionString, string requête) {
  60. ...
  61.         }
  62.
  63.         // affichage reader
  64.         static void AfficheReader(IDataReader reader) {
  65. ...
  66.         }
  67.     }
  68. }
  • lignes 14-17 : création d'un tableau de 5 articles
  • ligne 22 : la table articles est vidée
  • ligne 23 : elle est remplie avec les 5 acticles
  • ligne 24 : elle est affichée
  • ligne 26 : demande le prix moyen des articles
  • ligne 29 : demande le nombre d'articles
  • ligne 49 : utilisation de la méthode [IDbCommand].ExecuteScalar() pour calculer chacune de ces valeurs.

Les résultats de l'exécution sont les suivants :

Cacher Sélectionnez
  1. Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]
  2.
  3. Il y a eu 5 ligne(s) modifiée(s)
  4. transaction validée...
  5.
  6. ------------------------------------
  7. ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
  8. ------------------------------------
  9.
  10. 145 article1 100 10 1
  11. 146 article2 200 20 2
  12. 147 article3 300 30 3
  13. 148 article4 400 40 4
  14. 149 article5 500 50 5
  15. Prix moyen des articles=300
  16. Nombre d'articles=5

Les lignes 15 et 16 montrent les deux valeurs renvoyées par la méthode ExecuteScalar .

X-H. Application exemple - version 7

On reprend l'application exemple IMPOTS. La dernière version a été étudiée au paragraphe , page . C'était l'application à trois couches suivante :

Image non disponible
  • la couche [ui] était une interface graphique [A] et la couche [dao] trouvait ses données dans un fichier texte [B].
  • l'instanciation des couches et leur intégration dans l'application étaient assurées par Spring.
Image non disponible

Nous modifions la couche [dao] afin qu'elle aille chercher ses données dans une base de données.

X-H-1. La base de données

Le contenu du fichier texte [B] précédent est mis dans une base de données MySQL5. Nous montrons comment procéder :

Image non disponible
  • [1] :MySQL Administrator a été lancé
  • [2,3] : dans la zone [Schemata], cliquer droit et prendre l'option [Create Schema] pour créer une nouvelle base
  • [4] : la base s'appellera [bdimpots]
  • [5] : elle a été ajoutée aux bases de la zone [Schemata].
Image non disponible
  • [6,7] : cliquer droit sur la table et prendre l'option [Create New Table] pour créer une table
  • [8] : la table s'appellera [tranches]. Elle aura les colonnes [id, limite, coeffR, coeffN].
  • [9,10] : [id] est clé primaire de type INTEGER et a l'attribut AUTO_INCREMENT [10] : c'est le SGBD qui se chargera de remplir cette colonne lors d'ajout de lignes.
  • les colonnes [limite, coeffR, coeffN] sont de type DOUBLE.
  • [11,12] : la nouvelle table apparaît dans l'onglet [Schema Tables] de la base de données.
Image non disponible
  • [13,14] : pour mettre des données dans la table
  • [15] : [Query Browser] a été lancé
  • [16] : les données ont été entrées et validées pour les colonnes [limite, coeffR, coeffN]. La colonne [id] a été remplie par le SGBD. La validation a eu lieu avec [17].
Image non disponible
  • toujours dans [Query Browser] [18], on exécute [20] la requête [19]. Celle-ci crée un utilisateur ' admimpots ' de mot de passe ' mdpimpots ' et lui donne tous les privilèges ( grant all privileges ) sur tous les objets de la base bdimpots ( on bdimpots.* ). Cela va nous permettre de travailler sur la base [bdimpots] avec l'utilisateur [admimpots] plutôt qu'avec l'administrateur [root].

X-H-2. La solution Visual Studio

Image non disponible

Nous suivrons la démarche étudiée pour la version 5 de l'application exemple (cf paragraphe , page ). Nous allons construire progressivement la solution Visual Studio suivante :

Image non disponible
  • en [1] : la solution ImpotsV7 est formée de trois projets, un pour chacune des trois couches de l'application
  • en [2] : le projet [dao] de la couche [dao] qui va désormais exploiter une base de données
  • en [3] : le projet [metier] de la couche [metier]. Nous reprenons ici la couche [metier] de la version 5, décrite au paragraphe , page .
  • en [4] : le projet [ui] de la couche [ui]. Nous reprenons ici la couche [ui] de la version 6, décrite au paragraphe , page .

Nous nous appuyons sur l'acquis pour récupérer deux couches déjà écrites, les couches [ui] et [metier]. Cela est rendu possible par l'architecture en couches choisie. Nous aurons néanmoins besoin des codes source des couches [ui] et [metier]. Il n'est en effet pas possible de se contenter des DLL des couches. Lorsque dans la version 5, la DLL de la couche [metier] a été créée, elle avait une dépendance sur la DLL de la couche [dao]. Cette dépendance a été inscrite en dur dans la DLL de la couche [metier] (nom de la DLL de la couche [dao], version, jeton d'identité…). Ainsi la DLL de la version 5 [ImpotsV5-metier.dll] n'accepte de travailler qu'avec la DLL [ImpotsV5-dao.dll] avec laquelle elle a été compilée. Si on change la DLL de la couche [dao] il faut recompiler la couche [metier] pour lui créer une nouvelle DLL. Il en est de même pour la couche [ui]. Les couches [ui] et [metier] ne seront donc pas modifiées mais elles seront recompilées pour travailler avec la DLL de la nouvelle couche [dao].

X-H-3. La couche [dao]

Image non disponible
Image non disponible

Les références du projet (cf [1] dans le projet)

  • nunit.framework : pour le test NUnit
  • System.Configuration : pour exploiter le fichier de configuration [App.config]
  • System.Data : parce qu'on exploite une base de données.

Les entités (cf [2] dans le projet)

Les classes [TrancheImpot] et [ImpotException] sont celles des versions précédentes.

La couche [dao] (cf [3] dans le projet)

L'interface [IImpotDao] n'a pas changé :

Cacher Sélectionnez
  1. using Entites;
  2.
  3. namespace Dao {
  4.     public interface IImpotDao {
  5.         // les tranches d'impôt
  6.         TrancheImpot[] TranchesImpot{get;}
  7.     }
  8. }

La classe d'implémentation [DataBaseImpot] de cette interface est la suivante :

Cacher Sélectionnez
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Data.Common;
  4. using Entites;
  5.
  6. namespace Dao {
  7.     public class DataBaseImpot : IImpotDao {
  8.         // tranches d'impôt
  9.         private TrancheImpot[] tranchesImpot;
  10.         public TrancheImpot[] TranchesImpot { get { return tranchesImpot; } }
  11.
  12.         // constructeur
  13.         public DataBaseImpot(string factory, string connectionString, string requête) {
  14.             // factory : la factory du SGBD cible
  15.             // connectionString : la chaîne de connexion à la base des tranches d'impot
  16.             // on gère les éventuelles exceptions
  17.             try {
  18.                 // on récupère un connecteur générique pour le SGBD
  19.                 DbProviderFactory connecteur = DbProviderFactories.GetFactory(factory);
  20.                 using (DbConnection connexion = connecteur.CreateConnection()) {
  21.                     // configuration connexion
  22.                     connexion.ConnectionString = connectionString;
  23.                     // ouverture connexion
  24.                     connexion.Open();
  25.                     // configuration Command
  26.                     DbCommand sqlCommand = connecteur.CreateCommand();
  27.                     sqlCommand.CommandText = requête;
  28.                     sqlCommand.Connection = connexion;
  29.                     // exécution requête
  30.                     List<TrancheImpot> listTrancheImpot = new List<TrancheImpot>();
  31.                     using (DbDataReader reader = sqlCommand.ExecuteReader()) {
  32.                         while (reader.Read()) {
  33.                             // on crée une nouvelle trance d'impôt
  34.                             listTrancheImpot.Add(new TrancheImpot() { Limite = reader.GetDecimal(0), CoeffR = reader.GetDecimal(1), CoeffN = reader.GetDecimal(2) });
  35.                         }
  36.                     }
  37.                     // on met les tranches d'impôt dans son instance
  38.                     tranchesImpot = listTrancheImpot.ToArray();
  39.                 }
  40.             } catch (Exception ex) {
  41.                 // on encapsule l'exception dans un type ImpotException
  42.                 throw new ImpotException("Erreur de lecture des tranches d'impôt", ex) { Code = 101 };
  43.             }
  44.
  45.         }
  46.     }
  47. }
  • ligne 7 : la classe [DataBaseImpot] implémente l'interface [IImpotDao].
  • ligne 10 : l'implémentation de la méthode [TranchesImpot] de l'interface. Elle se contente de rendre une référence sur le tableau des tranches d'impôt de la ligne 9. Ce tableau va être construit par le constructeur de la classe.
  • ligne 13 : le constructeur. Il utilise un connecteur générique (cf paragraphe , page ) pour exploiter la base de données des tranches d'impôt. Le constructeur reçoit trois paramètres :
  • ligne 19 : demande un connecteur à la "factory"
  • ligne 20 : crée une connexion avec ce connecteur. Elle est créée mais pas encore opérationnelle
  • ligne 22 : la chaîne de connexion de la connexion est initialisée. On peut désormais se connecter.
  • ligne 24 : on se connecte
  • ligne 26 : demande au connecteur, un objet [DbCommand] pour exécuter un ordre SQL
  • ligne 27 : fixe l'ordre SQL à exécuter
  • ligne 28 : fixe la connexion sur laquelle l'exécuter
  • ligne 30 : une liste [listTrancheImpot] d'objets de type [TrancheImpot] est créée vide.
  • ligne 31 : l'ordre SQL Select est exécuté
  • lignes 32-35 : l'objet [DbDataReader] résultat du Select est exploité. Chaque ligne de la table résultat du Select sert à instancier un objet de type [TrancheImpot] qui est ajouté à la liste [listTrancheImpot].
  • ligne 38 : la liste d'objets de type [TrancheImpot] est transférée dans le tableau de la ligne 9.
  • lignes 40-43 : une éventuelle exception est encapsulée dans un type [ImpotException] et se voit attribuer le code d'erreur 101 (arbitraire).
  • le nom de la "factory" auprès de laquelle il va demander les classes pour se connecter à la base, émettre des ordres SQL, exploiter le résultat d'un Select.
  • la chaîne de connexion qu'il doit utiliser pour se connecter à la base de données
  • l'ordre SQL Select qu'il doit excécuter pour avoir les tranches d'impôt.

Le test [Test1] (cf [4] dans le projet)

La classe [Test1] se contente d'afficher les tranches d'impôt à l'écran. C'est celle déjà utilisée dans la version 5 (page ) sauf pour l'instruction qui instancie la couche [dao] (ligne 14).

Cacher Sélectionnez
  1. using System;
  2. using Dao;
  3. using Entites;
  4. using System.Configuration;
  5.
  6. namespace Tests {
  7.     class Test1 {
  8.         static void Main() {
  9.
  10.             // on crée la couche [dao]
  11.             IImpotDao dao = null;
  12.             try {
  13.                 // création couche [dao]
  14.                 dao = new DataBaseImpot(ConfigurationManager.AppSettings["factoryMySql5"], ConfigurationManager.ConnectionStrings["dbImpotsMySql5"].ConnectionString, ConfigurationManager.AppSettings["requete"]);
  15.             } catch (ImpotException e) {
  16.                 // affichage erreur
  17.                 string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
  18.                 Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
  19.                 // arrêt programme
  20.                 Environment.Exit(1);
  21.             }
  22.             // on affiche les tranches d'impôt
  23.             TrancheImpot[] tranchesImpot = dao.TranchesImpot;
  24.             foreach (TrancheImpot t in tranchesImpot) {
  25.                 Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
  26.             }
  27.         }
  28.     }
  29. }

La ligne 14 exploite le fichier de configuration [App.config] suivant :

Cacher Sélectionnez
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.     <connectionStrings>
  4.         <add name="dbImpotsMySql5" connectionString="Server=localhost;Database=bdimpots;Uid=admimpots;Pwd=mdpimpots;" />
  5.     </connectionStrings>
  6.     <appSettings>
  7.         <add key="requete" value="select limite, coeffr, coeffn from tranches"/>
  8.         <add key="factoryMySql5" value="MySql.Data.MySqlClient"/>
  9.     </appSettings>
  10. </configuration>
  • ligne 4 : la chaîne de connexion à la base MySQL5. On notera que c'est l'utilisateur [admimpots] qui établira la connexion.
  • ligne 8 : la "factory" pour travailler avec le SGBD MySQL5
  • ligne 7 : la requête SQL Select pour obtenir les tranches d'impôt.

Le projet est configuré pour exécuter [Test1.cs] :

Image non disponible

L'exécution du test donne les résultats suivants :

Cacher Sélectionnez
  1. 4962:0:0
  2. 8382:0,068:291,09
  3. 14753:0,191:1322,92
  4. 23888:0,283:2668,39
  5. 38868:0,374:4846,98
  6. 47932:0,426:6883,66
  7. 0:0,481:9505,54

Le test NUnit [NUnit1] (cf [4] dans le projet)

Le test unitaire [NUnit1] est celui déjà utilisé dans la version 5 (page ) sauf pour l'instruction qui instancie la couche [dao] (ligne 16).

Cacher Sélectionnez
  1. using System;
  2. using System.Configuration;
  3. using Dao;
  4. using Entites;
  5. using NUnit.Framework;
  6.
  7. namespace Tests {
  8.     [TestFixture]
  9.     public class NUnit1 : AssertionHelper{
  10.         // couche [dao] à tester
  11.         private IImpotDao dao;
  12.
  13.         // constructeur
  14.         public NUnit1() {
  15.             // initialisation couche [dao]
  16.             dao = new DataBaseImpot(ConfigurationManager.AppSettings["factoryMySql5"], ConfigurationManager.ConnectionStrings["dbImpotsMySql5"].ConnectionString, ConfigurationManager.AppSettings["requete"]);
  17.         }
  18.
  19.         // test
  20.         [Test]
  21.         public void ShowTranchesImpot(){
  22.             // on affiche les tranches d'impôt
  23.             TrancheImpot[] tranchesImpot = dao.TranchesImpot;
  24.             foreach (TrancheImpot t in tranchesImpot) {
  25.                 Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
  26.             }
  27.             // qqs tests
  28.             Expect(tranchesImpot.Length,EqualTo(7));
  29.             Expect(tranchesImpot[2].Limite,EqualTo(14753).Within(1e-6));
  30.             Expect(tranchesImpot[2].CoeffR, EqualTo(0.191).Within(1e-6));
  31.             Expect(tranchesImpot[2].CoeffN, EqualTo(1322.92).Within(1e-6));
  32.         }
  33.     }
  34. }

Pour exécuter ce test unitaire, le projet doit être de type [Class Library] :

Image non disponible
  • en [1] : la nature du projet a été changée
  • en [2] : la DLL générée s'appellera []
  • en [3] : après génération (F6) du projet, le dossier [dao/bin/Release] contient la DLL [ImpotsV7-dao.dll]. Il contient aussi le fichier de configuration [App.config] renommé [nom DLL].config. C'est standard dans Visual studio.

La DLL [ImpotsV7-dao.dll] est ensuite chargée dans le framework NUnit et exécutée :

Image non disponible
  • en [1] : les tests ont été réussis. Nous considérons désormais la couche [dao] opérationnelle. Sa DLL contient toutes les classes du projet dont les classes de test. Celles-ci sont inutiles. Nous reconstruisons la DLL afin d'en exclure les classes de tests.
  • en [2] : le dossier [tests] est exclu du projet
  • en [3] : le nouveau projet. Celui-ci est régénéré par F6 afin de générer une nouvelle DLL. C'est cette DLL qui sera utilisée par les couches [metier] et [ui] de l'application.

X-H-4. La couche [metier]

Image non disponible
Image non disponible
  • en [1], le projet [metier] est devenu le projet actif de la solution
  • en [2] : les références du projet. On notera la référence sur la DLL de la couche [dao] créée précédemment. Cette procédure d'ajout de référence a été décrite dans la version 5, au paragraphe , page .
  • en [3] : la couche [metier]. C'est celle de la version 5, décrite au paragraphe , page .

Le projet [metier] est configuré pour générer une DLL :

Image non disponible
  • [1] : le projet est de type "bibliothèque de classes"
  • [2] : la génération du projet produira la DLL [ImpotsV7-metier.dll] [3].

Le projet est généré (F6).

X-H-5. La couche [ui]

Image non disponible
Image non disponible
  • en [1], le projet [ui] est devenu le projet actif de la solution
  • en [2] : les références du projet. On notera les références sur les DLL des couches [dao] et [metier].
  • en [3] : la couche [ui]. C'est celle de la version 6 décrite au paragraphe , page .
  • en [4], le fichier de configuration [App.config] est analogue à celui de la version 6. Il n'en diffère que par la façon dont la couche [dao] est instanciée par Spring :
Cacher Sélectionnez
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.
  4.     <configSections>
  5.         <sectionGroup name="spring">
  6.             <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
  7.             <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
  8.         </sectionGroup>
  9.     </configSections>
  10.
  11.     <spring>
  12.         <context>
  13.             <resource uri="config://spring/objects" />
  14.         </context>
  15.         <objects xmlns="http://www.springframework.net">
  16.             <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
  17.                 <constructor-arg index="0" value="MySql.Data.MySqlClient"/>
  18.                 <constructor-arg index="1" value="Server=localhost;Database=bdimpots;Uid=admimpots;Pwd=mdpimpots;"/>
  19.                 <constructor-arg index="2" value="select limite, coeffr, coeffn from tranches"/>
  20.             </object>
  21.             <object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
  22.                 <constructor-arg index="0" ref="dao"/>
  23.             </object>
  24.         </objects>
  25.     </spring>
  26. </configuration>
  • lignes 11-25 : la configuration Spring
  • lignes 15-24 : les objets instanciés par Spring
  • lignes 16-20 : instanciation de la couche [dao]
  • ligne 16 : la couche [dao] est instanciée par la classe [Dao.DataBaseImpot] qui se trouve dans la DLL [ImpotsV7-Dao]
  • lignes 17-19 : les trois paramètres (factory du SGBD utilisé, chaîne de connexion, requête SQL) à fournir au constructeur de la classe [Dao.DataBaseImpot]
  • lignes 21-23 : instanciation de la couche [metier]. C'est la même configuration que dans la version 6.

Tests

Le projet [ui] est configuré comme suit :

Image non disponible
  • [1] : le projet est de type "Windows Application"
  • [2] : la génération du projet produira l'exécutable [ImpotsV7-ui.exe]

Un exemple d'exécution est donné en [3].

X-H-6. Changer la base de données

Image non disponible

La couche [dao] ci-dessus a été écrite avec un connecteur générique et une base MySQL5. Nous nous proposons ici de passer à une base SQL Server Compact afin de montrer que seule la configuration va changer.

La base sera la suivante :

Image non disponible
  • [1] : la base [dbimpots.sdf] dans la vue [DataBase Explorer] de Visual studio [2]. Elle a été créée sans mot de passe.
  • [3] : la table [data] qui contient les données. On a volontairement choisi des noms différents pour la table et les colonnes de ceux utilisés avec la base MySQL5 afin d'insister de nouveau sur l'intérêt de mettre ce genre de détails dans le fichier de configuration plutôt que dans le code.
  • [4] : la colonne [id] est clé primaire et a l'attribut Identity : c'est le SGBD qui va lui attribuer ses valeurs.
  • [5] : le contenu de la table [data].
Image non disponible
  • [6] : la base [dbimpots.sdf] a été placée dans le dossier du projet [ui] et intégrée à ce projet.
  • [7] : la base [dbimpots.sdf] sera copiée dans le dossier d'exécution du projet.

Le fichier de configuration [App.config] pour la nouvelle base de données est le suivant :

Cacher Sélectionnez
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.
  4.     <configSections>
  5.         <sectionGroup name="spring">
  6.             <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
  7.             <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
  8.         </sectionGroup>
  9.     </configSections>
  10.
  11.     <spring>
  12.         <context>
  13.             <resource uri="config://spring/objects" />
  14.         </context>
  15.         <objects xmlns="http://www.springframework.net">
  16.             <!--
  17.             <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
  18.                 <constructor-arg index="0" value="MySql.Data.MySqlClient"/>
  19.                 <constructor-arg index="1" value="Server=localhost;Database=bdimpots;Uid=admimpots;Pwd=mdpimpots;"/>
  20.                 <constructor-arg index="2" value="select limite, coeffr, coeffn from tranches"/>
  21.             </object>
  22.             -->
  23.             <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
  24.                 <constructor-arg index="0" value="System.Data.SqlServerCe.3.5"/>
  25.                 <constructor-arg index="1" value="Data Source=|DataDirectory|\dbimpots.sdf;" />
  26.                 <constructor-arg index="2" value="select data1, data2, data3 from data"/>
  27.             </object>
  28.             <object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
  29.                 <constructor-arg index="0" ref="dao"/>
  30.             </object>
  31.         </objects>
  32.     </spring>
  33. </configuration>
  • lignes 23-27 : la configuration de la couche [dao] pour exploiter la base [dbimpots.sdf].

Les résultats de l'exécution sont identiques aux précédents. On notera l'intérêt d'utiliser un connecteur générique pour rendre la couche [dao] insensible au changement de SGBD. Nous avons vu cependant que ce connecteur ne convenait pas à toutes les situations, notamment celles où des requêtes paramétrées sont utilisées. Il y a alors d'autres solutions telle celle évoquée, des frameworks tiers d'accès aux données (Spring, iBatis, NHibernate, LINQ…).

X-I. Pour aller plus loin …

  • LINQ est présenté dans de nombreux ouvrages, notamment dans le livre : C# 3.0 in a Nutshell, Joseph et Ben Albahari, éditions O'Reilly déjà cité dans l'introduction de ce document.
  • iBatis est présenté dans le livre : iBatis in Action, Clinton Begin, éditions Manning
  • Nhibernate in Action aux éditions Manning est prévu pour juillet 2008

Spring, iBatis, NHibernate ont des manuels de référence disponibles sur le site de ces différents frameworks.


CSharp tutoriel 2008

Apprentissage du langage C#

  • XI. Les threads d'exécution
    • XI-A. La classe Thread
    • XI-B. Création de threads d'exécution
    • XI-C. Intérêt des threads
    • XI-D. Echange d'informations entre threads
    • XI-E. Accès concurrents à des ressources partagées
      • XI-E-1. Accès concurrents non synchronisés
      • XI-E-2. La clause lock
      • XI-E-3. La classe Mutex
      • XI-E-4. La classe AutoResetEvent
      • XI-E-5. La classe Interlocked
    • XI-F. Accès concurrents à des ressources partagées multiples
      • XI-F-1. Un exemple
      • XI-F-2. La classe Monitor
    • XI-G. Les pools de threads
    • XI-H. La classe BackgroundWorker
      • XI-H-1. Exemple 1
      • XI-H-2. Exemple 2
    • XI-I. Données locales à un thread
      • XI-I-1. Le principe
      • XI-I-2. Application du principe
      • XI-I-3. Conclusion
    • XI-J. Pour approfondir…

XI. Les threads d'exécution

XI-A. La classe Thread

Lorsqu'on lance une application, elle s'exécute dans un flux d'exécution appelé un thread. La classe .NET modélisant un thread est la classe System.Threading.Thread et a la définition suivante :

Constructeurs

Image non disponible

Nous n'utiliserons dans les exemples à suivre que les constructeurs [1,3]. Le constructeur [1] admet comme paramètre une méthode ayant la signature [2], c.a.d. ayant un paramètre de type object et ne rendant pas de résultat. Le constructeur [3] admet comme paramètre une méthode ayant la signature [4], c.a.d. n'ayant pas de paramètre et ne rendant pas de résultat.

Propriétés

Quelques propriétés utiles :

  • Thread CurrentThread : propriété statique qui donne une référence sur le thread dans lequel se trouve le code ayant demandé cette propriété
  • string Name : le nom du thread
  • bool IsAlive : indique si le thread est en cours d'exécution ou non.

Méthodes

Les méthodes les plus utilisées sont les suivantes :

  • Start(), Start(object obj) : lance l'exécution asynchrone du thread, éventuellement en lui passant de l'information dans un type object .
  • Abort(), Abort(object obj) : pour terminer de force un thread
  • Join() : le thread T1 qui exécute T2.Join est bloqué jusqu'à ce que soit terminé le thread T2 . Il existe des variantes pour terminer l'attente au bout d'un temps déterminé.
  • Sleep(int n) : méthode statique - le thread exécutant la méthode est suspendu pendant n millisecondes. Il perd alors le processeur qui est donné à un autre thread.

Regardons une première application mettant en évidence l'existence d'un thread principal d'exécution, celui dans lequel s'exécute la fonction Main d'une classe :

Cacher Sélectionnez
  1. using System;
  2. using System.Threading;
  3.
  4. namespace Chap8 {
  5.     class Program {
  6.         static void Main(string[] args) {
  7.             // init thread courant
  8.             Thread main = Thread.CurrentThread;
  9.             // affichage
  10.             Console.WriteLine("Thread courant : {0}", main.Name);
  11.             // on change le nom
  12.             main.Name = "main";
  13.             // vérification
  14.             Console.WriteLine("Thread courant : {0}", main.Name);
  15.
  16.             // boucle infinie
  17.             while (true) {
  18.                 // affichage
  19.                 Console.WriteLine("{0} : {1:hh:mm:ss}", main.Name, DateTime.Now);
  20.                 // arrêt temporaire
  21.                 Thread.Sleep(1000);
  22.             }//while
  23.         }
  24.     }
  25. }
  • ligne 8 : on récupère une référence sur le thread dans lequel s'exécute la méthode [main]
  • lignes 10-14 : on affiche et on modifie son nom
  • lignes 17-22 : une boucle qui fait un affichage toutes les secondes
  • ligne 21 : le thread dans lequel s'exécute la méthode [main] va être suspendu pendant 1 seconde

Les résultats écran sont les suivants :

Cacher Sélectionnez
  1. Thread courant :
  2. Thread courant : main
  3. main : 04:19:00
  4. main : 04:19:01
  5. main : 04:19:02
  6. main : 04:19:03
  7. main : 04:19:04
  8. ^CAppuyez sur une touche pour continuer...
  • ligne 1 : le thread courant n'avait pas de nom
  • ligne 2 : il en a un
  • lignes 3-7 : l'affichage qui a lieu toutes les secondes
  • ligne 8 : le programme est interrompu par Ctrl-C.

XI-B. Création de threads d'exécution

Il est possible d'avoir des applications où des morceaux de code s'exécutent de façon "simultanée" dans différents threads d'exécution. Lorsqu'on dit que des thread s s'exécutent de façon simultanée, on commet souvent un abus de langage. Si la machine n'a qu'un processeur comme c'est encore souvent le cas, les thread s se partagent ce processeur : ils en disposent, chacun leur tour, pendant un court instant (quelques millisecondes). C'est ce qui donne l'illusion du parallélisme d'exécution. La portion de temps accordée à un thread dépend de divers facteurs dont sa priorité qui a une valeur par défaut mais qui peut être fixée également par programmation. Lorsqu'un thread dispose du processeur, il l'utilise normalement pendant tout le temps qui lui a été accordé. Cependant, il peut le libérer avant terme :

  • en se mettant en attente d'un événement ( Wait, Join )
  • en se mettant en sommeil pendant un temps déterminé ( Sleep )
  • Un thread T est tout d'abord créé par l'un des constructeurs présentés plus haut, par exemple :
    où Start est une méthode ayant l'une des deux signatures suivantes :
Cacher Sélectionnez
  1. void Start();
  2. void Start(object obj);
Cacher Sélectionnez
  1. Thread thread=new Thread(Start);
  • La création d'un thread ne lance pas celui-ci.
  • L'exécution du thread T est lancé par T.Start() : la méthode Start passée au constructeur de T va alors être exécutée par le thread T. Le programme qui exécute l'instruction T.Start() n'attend pas la fin de la tâche T : il passe aussitôt à l'instruction qui suit. On a alors deux tâches qui s'exécutent en parallèle. Elles doivent souvent pouvoir communiquer entre elles pour savoir où en est le travail commun à réaliser. C'est le problème de synchronisation des threads.
  • Une fois lancé, le thread T s'exécute de façon autonome. Il s'arrêtera lorsque la méthode S tart qu'il exécute aura fini son travail.
  • On peut forcer le thread T à se terminer :
    1. T.Abort() demande au thread T de se terminer.
  • On peut aussi attendre la fin de son exécution par T.Join(). On a là une instruction bloquante : le programme qui l'exécute est bloqué jusqu'à ce que la tâche T ait terminé son travail. C'est un moyen de synchronisation.

Examinons le programme suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Threading;
  3.
  4. namespace Chap8 {
  5.     class Program {
  6.         public static void Main() {
  7.             // init Thread courant
  8.             Thread main = Thread.CurrentThread;
  9.             // on fixe un nom au Thread
  10.             main.Name = "Main";
  11.
  12.             // création de threads d'exécution
  13.             Thread[] tâches = new Thread[5];
  14.             for (int i = 0; i < tâches.Length; i++) {
  15.                 // on crée le thread i
  16.                 tâches[i] = new Thread(Affiche);
  17.                 // on fixe le nom du thread
  18.                 tâches[i].Name =  i.ToString();
  19.                 // on lance l'exécution du thread i
  20.                 tâches[i].Start();
  21.             }
  22.
  23.             // fin de main
  24.             Console.WriteLine("Fin du thread {0} à {1:hh:mm:ss}",main.Name,DateTime.Now);
  25.         }
  26.
  27.         public static void Affiche() {
  28.             // affichage début d'exécution
  29.             Console.WriteLine("Début d'exécution de la méthode Affiche dans le Thread {0} : {1:hh:mm:ss}",Thread.CurrentThread.Name,DateTime.Now);
  30.             // mise en sommeil pendant 1 s
  31.             Thread.Sleep(1000);
  32.             // affichage fin d'exécution
  33.             Console.WriteLine("Fin d'exécution de la méthode Affiche dans le Thread {0} : {1:hh:mm:ss}", Thread.CurrentThread.Name, DateTime.Now);
  34.         }
  35.     }
  36. }
  • lignes 8-10 : on donne un nom au thread qui exécute la méthode [Main]
  • lignes 13-21 : on crée 5 threads et on les exécute. Les références des threads sont mémorisées dans un tableau afin de pouvoir les récupérer ultérieurement. Chaque thread exécute la méthode Affiche des lignes 27-35.
  • ligne 20 : le thread n° i est lancé. Cette opération est non bloquante. Le thread n° i va s'exécuter en parallèle du thread de la méthode [Main] qui l'a lancé.
  • ligne 24 : le thread qui exécute la méthode [Main] se termine.
  • lignes 27-35 : la méthode [Affiche] fait des affichages. Elle affiche le nom du thread qui l'exécute ainsi que les heures de début et fin d'exécution.
  • ligne 31 : tout thread exécutant la méthode [Affiche] va s'arrêter pendant 1 seconde. Le processeur va alors être donné à un autre thread en attente de processeur. A la fin de la seconde d'arrêt, le thread arrêté va âtre candidat au processeur. Il l'aura lorsque son tour sera venu. Cela dépend de divers facteurs dont la priorité des autres threads en attente de processeur.

Les résultats sont les suivants :

Cacher Sélectionnez
  1. Début d'exécution de la méthode Affiche dans le Thread 0 : 10:30:44
  2. Début d'exécution de la méthode Affiche dans le Thread 1 : 10:30:44
  3. Début d'exécution de la méthode Affiche dans le Thread 2 : 10:30:44
  4. Début d'exécution de la méthode Affiche dans le Thread 3 : 10:30:44
  5. Début d'exécution de la méthode Affiche dans le Thread 4 : 10:30:44
  6. Fin du thread Main à 10:30:44
  7. Fin d'exécution de la méthode Affiche dans le Thread 0 : 10:30:45
  8. Fin d'exécution de la méthode Affiche dans le Thread 1 : 10:30:45
  9. Fin d'exécution de la méthode Affiche dans le Thread 2 : 10:30:45
  10. Fin d'exécution de la méthode Affiche dans le Thread 3 : 10:30:45
  11. Fin d'exécution de la méthode Affiche dans le Thread 4 : 10:30:45

Ces résultats sont très instructifs :

  • on voit tout d'abord que le lancement de l'exécution d'un thread n'est pas bloquante. La méthode Main a lancé l'exécution de 5 threads en parallèle et a terminé son exécution avant eux. L'opération
Cacher Sélectionnez
  1.                 // on lance l'exécution du thread i
  2.                 tâches[i].Start();

lance l'exécution du thread tâches[i] mais ceci fait, l'exécution se poursuit immédiatement avec l'instruction qui suit sans attendre la fin d'exécution du thread.

  • tous les threads créés doivent exécuter la méthode A ffiche . L'ordre d'exécution est imprévisible. Même si dans l'exemple, l'ordre d'exécution semble suivre l'ordre des demandes d'exécution, on ne peut en conclure de généralités. Le système d'exploitation a ici 6 threads et un processeur. Il va distribuer le processeur à ces 6 threads selon des règles qui lui sont propres.
  • on voit dans les résultats une conséquence de la méthode Sleep . Dans l'exemple, c'est le thread 0 qui exécute le premier la méthode A ffiche . Le message de début d'exécution est affiché puis il exécute la méthode Sleep qui le suspend pendant 1 seconde. Il perd alors le processeur qui devient ainsi disponible pour un autre thread. L'exemple montre que c'est le thread 1 qui va l'obtenir. Le thread 1 va suivre le même parcours ainsi que les autres threads. Lorsque la seconde de sommeil du thread 0 va être terminée, son exécution peut reprendre. Le système lui donne le processeur et il peut terminer l'exécution de la méthode A ffiche .

Modifions notre programme pour terminer la méthode Main par les instructions :

Cacher Sélectionnez
  1.             // fin de main
  2.             Console.WriteLine("Fin du thread " + main.Name);
  3.             // on arrête tous les threads
  4. Environment.Exit(0);

L'exécution du nouveau programme donne les résultats suivants :

Cacher Sélectionnez
  1. Début d'exécution de la méthode Affiche dans le Thread 0 : 10:33:18
  2. Début d'exécution de la méthode Affiche dans le Thread 1 : 10:33:18
  3. Début d'exécution de la méthode Affiche dans le Thread 2 : 10:33:18
  4. Début d'exécution de la méthode Affiche dans le Thread 3 : 10:33:18
  5. Début d'exécution de la méthode Affiche dans le Thread 4 : 10:33:18
  6. Fin du thread Main à 10:33:18
  • lignes 1-5 : les threads créés par la fonction Main commencent leur exécution et sont interrompus pendant 1 seconde
  • ligne 6 : le thread [Main] récupère le processeur et exécute l'instruction :
Cacher Sélectionnez
  1.         Environment.Exit(0);

Cette instruction arrête tous les threads de l'application et non simplement le thread Main .

Si la méthode Main veut attendre la fin d'exécution des threads qu'elle a créés, elle peut utiliser la méthode Join de la classe Thread :

Cacher Sélectionnez
  1.         public static void Main() {
  2. ...
  3.             // on attend tous les threads
  4.             for (int i = 0; i < tâches.Length; i++) {
  5.                 // attente de la fin d'exécution du thread i
  6.                 tâches[i].Join();
  7.             }
  8.             // fin de main
  9.             Console.WriteLine("Fin du thread {0} à {1:hh:mm:ss}", main.Name, DateTime.Now);
  10. }
  • ligne 6 : le thread [Main] attend chacun des threads. Il est d'abord bloqué en attente du thread n° 1, puis du thread n° 2, etc… Au final lorsqu'il sort de la boucle des lignes 2-5, c'est ce que les 5 threads qu'il a lancés sont finis.

On obtient alors les résultats suivants :

Cacher Sélectionnez
  1. Début d'exécution de la méthode Affiche dans le Thread 0 : 10:35:18
  2. Début d'exécution de la méthode Affiche dans le Thread 1 : 10:35:18
  3. Début d'exécution de la méthode Affiche dans le Thread 2 : 10:35:18
  4. Début d'exécution de la méthode Affiche dans le Thread 3 : 10:35:18
  5. Début d'exécution de la méthode Affiche dans le Thread 4 : 10:35:18
  6. Fin d'exécution de la méthode Affiche dans le Thread 0 : 10:35:19
  7. Fin d'exécution de la méthode Affiche dans le Thread 1 : 10:35:19
  8. Fin d'exécution de la méthode Affiche dans le Thread 2 : 10:35:19
  9. Fin d'exécution de la méthode Affiche dans le Thread 3 : 10:35:19
  10. Fin d'exécution de la méthode Affiche dans le Thread 4 : 10:35:19
  11. Fin du thread Main à 10:35:19
  • ligne 11 : le thread [Main] s'est terminé après les threads qu'il avait lancés.

XI-C. Intérêt des threads

Maintenant que nous avons mis en évidence l'existence d'un thread par défaut, celui qui exécute la méthode Main , et que nous savons comment en créer d'autres, arrêtons-nous sur l'intérêt pour nous des threads et sur les raisons pour lesquelles nous les présentons ici. Il y a un type d'applications qui se prêtent bien à l'utilisation des threads, ce sont les applications client-serveur de l'internet. Nous allons les présenter dans le chapitre qui suit. Dans une application client-serveur de l'internet, un serveur situé sur une machine S1 répond aux demandes de clients situés sur des machines distantes C1, C2…, Cn.

Image non disponible

Nous utilisons tous les jours des applications de l'internet correspondant à ce schéma : services Web, messagerie électronique, consultation de forums, transfert de fichiers… Dans le schéma ci-dessus, le serveur S1 doit servir les clients Ci de façon simultanée. Si nous prenons l'exemple d'un serveur FTP (File Transfer Protocol) qui délivre des fichiers à ses clients, nous savons qu'un transfert de fichier peut prendre parfois plusieurs minutes. Il est bien sûr hors de question qu'un client monopolise tout seul le serveur pendant une telle durée. Ce qui est fait habituellement, c'est que le serveur crée autant de threads d'exécution qu'il y a de clients. Chaque thread est alors chargé de s'occuper d'un client particulier. Le processeur étant partagé cycliquement entre tous les threads actifs de la machine, le serveur passe alors un peu de temps avec chaque client assurant ainsi la simultanéité du service.

Image non disponible

Dans la pratique, le serveur utilise un pool de threads avec un nombre limité de threads, 50 par exemple. Le 51 ième client est alors prié d'attendre.

XI-D. Echange d'informations entre threads

Dans les exemples précédents, un thread était initialisé de la façon suivante :

Cacher Sélectionnez
  1. Thread t=new Thread(Run);

où Run était une méthode ayant la signature suivante :

Cacher Sélectionnez
  1. void Run();

Il est également possible d'utiliser la signature suivante :

Cacher Sélectionnez
  1. void Run(object obj);

Cela permet de transmettre de l'information au thread lancé. Ainsi

Cacher Sélectionnez
  1. t.Start(obj1);

va lancer le thread t qui va alors exécuter la méthode Run qui lui a été associée par construction, en lui passant le paramètre effectif obj1 . Voici un exemple :

Cacher Sélectionnez
  1. using System;
  2. using System.Threading;
  3.
  4. namespace Chap8 {
  5.     class Program4 {
  6.         public static void Main() {
  7.             // init Thread courant
  8.             Thread main = Thread.CurrentThread;
  9.             // on fixe un nom au Thread
  10.             main.Name = "Main";
  11.
  12.             // création de threads d'exécution
  13.             Thread[] tâches = new Thread[5];
  14.             Data[] data = new Data[5];
  15.             for (int i = 0; i < tâches.Length; i++) {
  16.                 // on crée le thread i
  17.                 tâches[i] = new Thread(Sleep);
  18.                 // on fixe le nom du thread
  19.                 tâches[i].Name = i.ToString();
  20.                 // on lance l'exécution du thread i
  21.                 tâches[i].Start(data[i] = new Data { Début = DateTime.Now, Durée = i+1 });
  22.             }
  23.             // on attend tous les threads
  24.             for (int i = 0; i < tâches.Length; i++) {
  25.                 // attente de la fin d'exécution du thread i
  26.                 tâches[i].Join();
  27.                 // affichage résultat
  28.                 Console.WriteLine("Thread {0} terminé : début {1:hh:mm:ss}, durée programmée {2} s, fin {3:hh:mm:ss}, durée effective {4}",
  29.                     tâches[i].Name,data[i].Début,data[i].Durée,data[i].Fin,(data[i].Fin-data[i].Début));
  30.             }
  31.             // fin de main
  32.             Console.WriteLine("Fin du thread {0} à {1:hh:mm:ss}", main.Name, DateTime.Now);
  33.         }
  34.
  35.         public static void Sleep(object infos) {
  36.             // on récupère le paramètre
  37.             Data data = (Data)infos;
  38.             // mise en sommeil pendant Durée secondes
  39.             Thread.Sleep(data.Durée*1000);
  40.             // fin d'exécution
  41.             data.Fin = DateTime.Now;
  42.         }
  43.     }
  44.
  45.     internal class Data {
  46.         // informations diverses
  47.         public DateTime Début { get; set; }
  48.         public int Durée { get; set; }
  49.         public DateTime Fin { get; set; }
  50.     }
  51. }
  • lignes 45-50 : l'information de type [Data] passée aux threads :
  • Début : heure du début de l'exécution du thread - fixée par le thread lanceur
  • Durée : durée en secondes du Sleep exécuté par le thread lancé - fixée par le thread lanceur
  • Fin : heure du début de l'exécution du thread - fixée par le thread lancé
    Il y a là un échange d'informations entre le thread lanceur et le thread lancé.
  • lignes 35-43 : la méthode Sleep exécutée par les threads a la signature void Sleep(object obj) . Le paramètre effectif obj sera du type [Data] défini ligne 45.
  • lignes 15-22 : création de 5 threads
  • ligne 17 : chaque thread est associé à la méthode Sleep de la ligne 35
  • ligne 21 : un objet de type [Data] est passé à la méthode Start qui lance le thread. Dans cet objet on a noté l'heure de début de l'exécution du thread ainsi que la durée en secondes pendant laquelle il doit dormir. Cet objet est mémorisé dans le tableau de la ligne 14.
  • lignes 24-30 : le thread [Main] attend la fin de tous les threads qu'il a lancés.
  • lignes 28-29 : le thread [Main] récupère l'objet data[i] du thread n° i et en affiche le contenu.
  • lignes 35-42 : la méthode Sleep exécutée par les threads
  • ligne 37 : on récupère le paramètre de type [Data]
  • ligne 39 : le champ Durée du paramètre est utilisé pour fixer la durée du Sleep
  • ligne 41 : le champ Fin du paramètre est initialisé

Les résultats de l'exécution sont les suivants :

Cacher Sélectionnez
  1. Thread 0 terminé : début 11:18:50, durée programmée 1 s, fin 11:18:51, durée effective 00:00:01.0156250
  2. Thread 1 terminé : début 11:18:50, durée programmée 2 s, fin 11:18:52, durée effective 00:00:02
  3. Thread 2 terminé : début 11:18:50, durée programmée 3 s, fin 11:18:53, durée effective 00:00:03
  4. Thread 3 terminé : début 11:18:50, durée programmée 4 s, fin 11:18:54, durée effective 00:00:04
  5. Thread 4 terminé : début 11:18:50, durée programmée 5 s, fin 11:18:55, durée effective 00:00:05
  6. Fin du thread Main à 11:18:55

Cet exemple montre que deux threads peuvent s'échanger de l'information :

  • le thread lanceur peut contrôler l'exécution du thread lancé en lui donnant des informations
  • le thread lancé peut rendre des résultats au thread lanceur.

Pour que le thread lancé sache à quel moment les résultats qu'il attend sont disponibles, il faut qu'il soit averti de la fin du thread lancé. Ici, il a attendu qu'il se termine en utilisant la méthode Join . Il y a d'autres façons de faire la même chose. Nous les verrons ultérieurement.

XI-E. Accès concurrents à des ressources partagées

XI-E-1. Accès concurrents non synchronisés

Dans le paragraphe sur l'échange d'informations entre threads, l'information échangée ne l'était que par deux threads et à des moments bien précis. On avait là un classique passage de paramètres. Il existe d'autres cas où une information est partagée par plusieurs threads qui peuvent vouloir la lire ou la mettre à jour au même moment. Se pose alors le problème de l'intégrité de cette information. Supposons que l'information partagée soit une structure S avec diverses informations I1, I2… In.

  • un thread T1 commence à mettre à jour la structure S : il modifie le champ I1 et est interrompu avant d'avoir terminé la mise à jour complète de la structure S
  • un thread T2 qui récupère le processeur lit alors la structure S pour prendre des décisions. Il lit une structure dans un état instable : certains champs sont à jour, d'autres pas.

On appelle cette situation, l'accès à une ressource partagée, ici la structure S, et elle est souvent assez délicate à gérer. Prenons l'exemple suivant pour illustrer les problèmes qui peuvent surgir :

  • une application va générer n threads, n étant passé en paramètre
  • la ressource partagée est un compteur qui devra être incrémenté par chaque thread généré
  • à la fin de l'application, la valeur du compteur est affichée. On devrait donc trouver n .

Le programme est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Threading;
  3.
  4. namespace Chap8 {
  5.     class Program {
  6.
  7.         // variables de classe
  8.         static int cptrThreads = 0;    // compteur de threads
  9.
  10.         //main
  11.         public static void Main(string[] args) {
  12.             // mode d'emploi
  13.             const string syntaxe = "pg nbThreads";
  14.             const int nbMaxThreads = 100;
  15.
  16.             // vérification nbre d'arguments
  17.             if (args.Length != 1) {
  18.                 // erreur
  19.                 Console.WriteLine(syntaxe);
  20.                 // arrêt
  21.                 Environment.Exit(1);
  22.             }
  23.             // vérification qualité de l'argument
  24.             int nbThreads = 0;
  25.             bool erreur = false;
  26.             try {
  27.                 nbThreads = int.Parse(args[0]);
  28.                 if (nbThreads < 1 || nbThreads > nbMaxThreads)
  29.                     erreur = true;
  30.             } catch {
  31.                 // erreur
  32.                 erreur = true;
  33.             }
  34.             // erreur ?
  35.             if (erreur) {
  36.                 // erreur
  37.                 Console.Error.WriteLine("Nombre de threads incorrect (entre 1 et 100)");
  38.                 // fin
  39.                 Environment.Exit(2);
  40.             }
  41.             // création et génération des threads
  42.             Thread[] threads = new Thread[nbThreads];
  43.             for (int i = 0; i < nbThreads; i++) {
  44.                 // création
  45.                 threads[i] = new Thread(Incrémente);
  46.                 // nommage
  47.                 threads[i].Name = "" + i;
  48.                 // lancement
  49.                 threads[i].Start();
  50.             }//for
  51.             // attente de la fin des threads
  52.             for (int i = 0; i < nbThreads; i++) {
  53.                 threads[i].Join();
  54.             }
  55.             // affichage compteur
  56.             Console.WriteLine("Nombre de threads générés : " + cptrThreads);
  57.         }
  58.
  59.         public static void Incrémente() {
  60.             // augmente le compteur de threads
  61.             // lecture compteur
  62.             int valeur = cptrThreads;
  63.             // suivi
  64.             Console.WriteLine("A {0:hh:mm:ss}, le thread {1}  a lu la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
  65.             // attente
  66.             Thread.Sleep(1000);
  67.             // incrémentation compteur
  68.             cptrThreads = valeur + 1;
  69.             // suivi
  70.             Console.WriteLine("A {0:hh:mm:ss}, le thread {1}  a écrit la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
  71.         }
  72.     }
  73. }

Nous ne nous attarderons pas sur la partie génération de threads déjà étudiée. Intéressons-nous plutôt à la méthode Incrémente , de la ligne 59 utilisée par chaque thread pour incrémenter le compteur statique cptrThreads de la ligne 8.

  • ligne 62 : le compteur est lu
  • ligne 66 : le thread s'arrête 1 s. Il perd donc le processeur
  • ligne 68 : le compteur est incrémenté

L'étape 2 n'est là que pour forcer le thread à perdre le processeur. Celui-ci va être donné à un autre thread. Dans la pratique, rien n'assure qu'un thread ne sera pas interrompu entre le moment où il va lire le compteur et le moment où il va l'incrémenter. Même si on écrit cptrThreads++ , donnant ainsi l'illusion d'une instruction unique, le risque existe de perdre le processeur entre le moment où on lit la valeur du compteur et celui on écrit sa valeur incrémentée de 1. En effet, l'opération de haut niveau cptrThreads++ va faire l'objet de plusieurs instructions élémentaires au niveau du processeur. L'étape 2 de sommeil d'une seconde n'est donc là que pour systématiser ce risque.

Les résultats obtenus avec 5 threads sont les suivants :

Cacher Sélectionnez
  1. A 12:00:56, le thread 3  a lu la valeur du compteur : 0
  2. A 12:00:56, le thread 2  a lu la valeur du compteur : 0
  3. A 12:00:56, le thread 1  a lu la valeur du compteur : 0
  4. A 12:00:56, le thread 0  a lu la valeur du compteur : 0
  5. A 12:00:56, le thread 4  a lu la valeur du compteur : 0
  6. A 12:00:57, le thread 3  a écrit la valeur du compteur : 1
  7. A 12:00:57, le thread 2  a écrit la valeur du compteur : 1
  8. A 12:00:57, le thread 1  a écrit la valeur du compteur : 1
  9. A 12:00:57, le thread 0  a écrit la valeur du compteur : 1
  10. A 12:00:57, le thread 4  a écrit la valeur du compteur : 1
  11. Nombre de threads générés : 1

A la lecture de ces résultats, on voit bien ce qui se passe :

  • ligne 1 : un premier thread lit le compteur. Il trouve 0. Il s'arrête 1 s donc perd le processeur
  • ligne 2 : un second thread prend alors le processeur et lit lui aussi la valeur du compteur. Elle est toujours à 0 puisque le thread précédent ne l'a pas encore incrémentée. Il s'arrête lui aussi 1 s et perd à son tour le processeur.
  • lignes 1-5 : en 1 s, les 5 threads ont le temps de passer tous et de lire tous la valeur 0.
  • lignes 6-10 : lorsqu'ils vont se réveiller les uns après les autres, ils vont incrémenter la valeur 0 qu'ils ont lue et écrire la valeur 1 dans le compteur, ce que confirme le programme principal (Main) en ligne 11.

D'où vient le problème ? Le second thread a lu une mauvaise valeur du fait que le premier avait été interrompu avant d'avoir terminé son travail qui était de mettre à jour le compteur dans la fenêtre. Cela nous amène à la notion de ressource critique et de section critique d'un programme:

  • une ressource critique est une ressource qui ne peut être détenue que par un thread à la fois. Ici la ressource critique est le compteur.
  • une section critique d'un programme est une séquence d'instructions dans le flux d'exécution d'un thread au cours de laquelle il accède à une ressource critique. On doit assurer qu'au cours de cette section critique, il est le seul à avoir accès à la ressource.

Dans notre exemple, la section critique est le code situé entre la lecture du compteur et l'écriture de sa nouvelle valeur :

Cacher Sélectionnez
  1.             // lecture compteur
  2.             int valeur = cptrThreads;
  3.             // attente
  4.             Thread.Sleep(1000);
  5.             // incrémentation compteur
  6. cptrThreads = valeur + 1;

Pour exécuter ce code, un thread doit être assuré d'être tout seul. Il peut être interrompu mais pendant cette interruption, un autre thread ne doit pas pouvoir exécuter ce même code. La plate-forme .NET offre divers outils pour assurer l'entrée unitaire dans les sections critiques de code. Nous en voyons quelques-uns maintenant.

XI-E-2. La clause lock

La clause lock permet de délimiter une section critique de la façon suivante :

Cacher Sélectionnez
  1. lock(obj){section critique}

obj doit être une référence d'objet visible par tous les threads exécutant la section critique. La clause lock assure qu'un seul thread à la fois exécutera la section critique. L'exemple précédent est réécrit comme suit :

Cacher Sélectionnez
  1. using System;
  2. using System.Threading;
  3.
  4. namespace Chap8 {
  5.     class Program2 {
  6.
  7.         // variables de classe
  8.         static int cptrThreads = 0;    // compteur de threads
  9.         static object synchro = new object(); // objet de synchronisation
  10.
  11.         //main
  12.         public static void Main(string[] args) {
  13.     ...
  14.             // attente de la fin des threads
  15.             Thread.CurrentThread.Name = "Main";
  16.             for (int i = nbThreads - 1; i >= 0; i--) {
  17.                 Console.WriteLine("A {0:hh:mm:ss}, le thread {1} attend la fin du thread {2}", DateTime.Now, Thread.CurrentThread.Name, threads[i].Name);
  18.                 threads[i].Join();
  19.                 Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a été prévenu de la fin du thread {2}", DateTime.Now, Thread.CurrentThread.Name, threads[i].Name);
  20.             }
  21.             // affichage compteur
  22.             Console.WriteLine("Nombre de threads générés : " + cptrThreads);
  23.         }
  24.
  25.         public static void Incrémente() {
  26.             // augmente le compteur de threads
  27.             // un accès exclusif au compteur est demandé
  28.             Console.WriteLine("A {0:hh:mm:ss}, le thread {1}  attend l'autorisation d'entrer dans la section critique", DateTime.Now, Thread.CurrentThread.Name);
  29.             lock (synchro) {
  30.                 // lecture compteur
  31.                 int valeur = cptrThreads;
  32.                 // suivi
  33.                 Console.WriteLine("A {0:hh:mm:ss}, le thread {1}  a lu la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
  34.                 // attente
  35.                 Thread.Sleep(1000);
  36.                 // incrémentation compteur
  37.                 cptrThreads = valeur + 1;
  38.                 // suivi
  39.                 Console.WriteLine("A {0:hh:mm:ss}, le thread {1}  a écrit la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads);
  40.             }
  41.             Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a quitté la section critique", DateTime.Now, Thread.CurrentThread.Name);
  42.         }
  43.     }
  44. }
  • ligne 9 : synchro est l'objet qui va permettre la synchronisation de tous les threads.
  • lignes 16-23 : la méthode [Main] attend les threads dans l'ordre inverse de leur création.
  • lignes 29-40 : la section critique de la méthode Incrémente a été encadrée par la clause lock .

Les résultats obtenus avec 3 threads sont les suivants :

Cacher Sélectionnez
  1. A 09:37:09, le thread 0 attend l'autorisation d'entrer dans la section critique
  2. A 09:37:09, le thread 0 a lu la valeur du compteur : 0
  3. A 09:37:09, le thread 1 attend l'autorisation d'entrer dans la section critique
  4. A 09:37:09, le thread 2 attend l'autorisation d'entrer dans la section critique
  5. A 09:37:09, le thread Main attend la fin du thread 2
  6. A 09:37:10, le thread 0 a écrit la valeur du compteur : 1
  7. A 09:37:10, le thread 1 a lu la valeur du compteur : 1
  8. A 09:37:10, le thread 0 a quitté la section critique
  9. A 09:37:11, le thread 1 a écrit la valeur du compteur : 2
  10. A 09:37:11, le thread 1 a quitté la section critique
  11. A 09:37:11, le thread 2 a lu la valeur du compteur : 2
  12. A 09:37:12, le thread 2 a écrit la valeur du compteur : 3
  13. A 09:37:12, le thread 2 a quitté la section critique
  14. A 09:37:12, le thread Main a été prévenu de la fin du thread 2
  15. A 09:37:12, le thread Main attend la fin du thread 1
  16. A 09:37:12, le thread Main a été prévenu de la fin du thread 1
  17. A 09:37:12, le thread Main attend la fin du thread 0
  18. A 09:37:12, le thread Main a été prévenu de la fin du thread 0
  19. Nombre de threads générés : 3
  • le thread 0 entre le 1er dans la section critique : lignes 1, 2, 6, 8
  • les deux autres threads vont être bloqués tant que le thread 0 ne sera pas sorti de la section critique : lignes 3 et 4
  • le thread 1 passe ensuite : lignes 7, 9, 10
  • le thread 2 passe ensuite : lignes 11, 12, 13
  • ligne 14 : le thread Main qui attendait la fin du thread 2 est prévenu
  • ligne 15 : le thread Main attend maintenant la fin du thread 1. Celui-ci est déjà terminé. Le thread Main en est prévenu immédiatement, ligne 16.
  • lignes 17-18 : le même processus se passe avec le thread 0
  • ligne 19 : le nombre de threads est correct

XI-E-3. La classe Mutex

La classe System.Threading.Mutex permet elle aussi de délimiter des sections critiques. Elle diffère de la clause lock en terme de visibilité :

  • la clause lock permet de synchroniser des threads d'une même application
  • la classe Mutex permet de synchroniser des threads de différentes applications.

Nous utiliserons le constructeur et les méthodes suivants :

public Mutex() | crée un Mutex M
public bool WaitOne() | Le thread T1 qui exécute l'opération M.WaitOne() demande la propriété de l'objet de synchronisation M. Si le Mutex M n'est détenu par aucun thread (le cas au départ), il est "donné" au thread T1 qui l'a demandé. Si un peu plus tard, un thread T2 fait la même opération, il sera bloqué. En effet, un Mutex ne peut appartenir qu'à un thread. Il sera débloqué lorsque le thread T1 libèrera le Mutex M qu'il détient. Plusieurs threads peuvent ainsi être bloqués en attente du Mutex M.
public void ReleaseMutex() | Le thread T1 qui effectue l'opération M.ReleaseMutex() abandonne la propriété du Mutex M. Lorsque le thread T1 perdra le processeur, le système pourra donner celui-ci à l'un des threads en attente du Mutex M. Un seul l'obtiendra à son tour, les autres en attente de M restant bloqués

Un Mutex M gère l'accès à une ressource partagée R. Un thread demande la ressource R par M.WaitOne() et la rend par M.ReleaseMutex() . Une section critique de code qui ne doit être exécutée que par un seul thread à la fois est une ressource partagée. La synchronisation d'exécution de la section critique peut se faire ainsi :

Cacher Sélectionnez
  1. M.WaitOne();
  2. // le thread est seul à entrer ici
  3. // section critique
  4. ....
  5. M.ReleaseMutex();

où M est un objet Mutex . Il ne faut pas oublier de libérer un Mutex devenu inutile afin qu'un autre thread puisse entrer dans la section critique, sinon les threads en attente du Mutex jamais libéré n'auront jamais accès au processeur.

Si nous mettons en pratique sur l'exemple précédent ce que nous venons de voir, notre application devient la suivante :

Cacher Sélectionnez
  1. using System;
  2. using System.Threading;
  3.
  4. namespace Chap8 {
  5.     class Program3 {
  6.
  7.         // variables de classe
  8.         static int cptrThreads = 0;    // compteur de threads
  9.         static Mutex synchro = new Mutex(); // objet de synchronisation
  10.
  11.         //main
  12.         public static void Main(string[] args) {
  13.     ...
  14.         }
  15.
  16.         public static void Incrémente() {
  17. ....
  18.             synchro.WaitOne();
  19.             try {
  20. ...
  21.             } finally {
  22. ...
  23.                 synchro.ReleaseMutex();
  24.             }
  25.         }
  26.     }
  27. }
  • ligne 9 : l'objet de synchronisation des threads est désormais un Mutex .
  • ligne 18 : début de la section critique - un seul thread doit y entrer. On se bloque jusqu'à ce que le Mutex synchro soit libre.
  • ligne 33 : parce qu'un Mutex doit toujours être libéré, exception ou pas, on gère la section critique avec un try / finally afin de libérer le Mutex dans le finally .
  • ligne 23 : le Mutex est libéré une fois la section critique passée.

Les résultats obtenus sont les mêmes que précédemment.

XI-E-4. La classe AutoResetEvent

Un objet AutoResetEvent est une barrière ne laissant passer qu'un thread à la fois, comme les deux outils précédents lock et Mutex . On construit un objet AutoResetEvent de la façon suivante :

Cacher Sélectionnez
  1. AutoResetEvent barrière=new AutoresetEvent(bool état);

Le booléen état indique l'état fermé (false) ou ouvert (true) de la barrière. Un thread voulant passer la barrière l'indiquera de la façon suivante :

Cacher Sélectionnez
  1. barrière.WaitOne();
  • si la barrière est ouverte, le thread passe et la barrière est refermée derrière lui. Si plusieurs threads attendaient, on est assuré qu'un seul passera.
  • si la barrière est fermée, le thread est bloqué. Un autre thread l'ouvrira lorsque le moment sera venu. Ce moment est entièrement dépendant du problème traité. La barrière sera ouverte par l'opération :
Cacher Sélectionnez
  1. barrière.Set();

Il peut arriver qu'un thread veuille fermer une barrière. Il pourra le faire par :

Cacher Sélectionnez
  1. barrière.Reset();

Si dans l'exemple précédent, on remplace l'objet Mutex par un objet de type AutoResetEvent , le code devient le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Threading;
  3.
  4. namespace Chap8 {
  5.     class Program4 {
  6.
  7.         // variables de classe
  8.         static int cptrThreads = 0;    // compteur de threads
  9.         static EventWaitHandle synchro = new AutoResetEvent(false); // objet de synchronisation
  10.
  11.         //main
  12.         public static void Main(string[] args) {
  13. ....
  14.             // on ouvre la barrière de la section critique
  15.             Console.WriteLine("A {0:hh:mm:ss}, le thread {1} ouvre la barrière de la section critique", DateTime.Now, Thread.CurrentThread.Name);
  16.             synchro.Set();
  17.             // attente de la fin des threads
  18. ...
  19.             // affichage compteur
  20.             Console.WriteLine("Nombre de threads générés : " + cptrThreads);
  21.         }
  22.
  23.         public static void Incrémente() {
  24.             // augmente le compteur de threads
  25.             // un accès exclusif au compteur est demandé
  26. ...
  27.             synchro.WaitOne();
  28.             try {
  29. ...
  30.             } finally {
  31.                 // on relâche la ressource
  32. ...
  33.                 synchro.Set();
  34.             }
  35.         }
  36.     }
  37. }
  • ligne 9 : la barrière est créée fermée. Elle sera ouverte par le thread Main ligne 16.
  • ligne 27 : le thread chargé d'incrémenter le compteur de threads demande l'autorisation d'entrer dans la section critique. Les différents threads vont s'accumuler devant la barrière fermée. Lorque le thread Main va l'ouvrir, l'un des threads en attente va passer.
  • ligne 33 : lorsqu'il a terminé son travail, il rouvre la barrière permettant à un autre thread d'entrer.

On obtient des résultats analogues aux précédents.

XI-E-5. La classe Interlocked

La classe Interlocked permet de rendre atomique un groupe d'opérations. Dans un groupe d'opérations atomique , soit toutes les opérations sont exécutées par le thread qui exécute le groupe soit aucune. On ne reste pas dans un état ou certaines ont été exécutées et d'autres pas. Les objets de synchronisation lock , Mutex , AutoResetEvent ont toutes pour but de rendre atomique un groupe d'opérations. Ce résultat est obtenu au prix du blocage de threads. La classe Interlocked permet, pour des opérations simples mais assez fréquentes, d'éviter le blocage de threads. La classe Interlocked offre les méthodes statiques suivantes :

Image non disponible

La méthode Increment a la signature suivante :

Cacher Sélectionnez
  1. public static int Increment(ref int location);

Elle permet d'incrémenter de 1 le paramètre location . L'opération est garantie atomique .

Notre programme de comptage de threads peut alors être le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Threading;
  3.
  4. namespace Chap8 {
  5.     class Program5 {
  6.
  7.         // variables de classe
  8.         static int cptrThreads = 0;    // compteur de threads
  9.
  10.         //main
  11.         public static void Main(string[] args) {
  12. ...
  13.         }
  14.
  15.         public static void Incrémente() {
  16.             // incrémente le compteur de threads
  17.             Interlocked.Increment(ref cptrThreads);
  18.         }
  19.     }
  20. }
  • ligne 17 : le compteur de threads est incrémenté de façon atomique.

XI-F. Accès concurrents à des ressources partagées multiples

XI-F-1. Un exemple

Dans nos exemples précédents, une unique ressource était partagée par les différents threads. La situation peut se compliquer s'il y en a plusieurs et qu'elles sont dépendantes les unes des autres. Une situation d'interblocage peut notamment survenir. Cette situation appelée également deadlock est celle dans laquelle deux threads s'attendent mutuellement. Considérons les actions suivantes qui se suivent dans le temps :

  • un thread T1 obtient la propriété d'un Mutex M1 pour avoir accès à une ressource partagée R1
  • un thread T2 obtient la propriété d'un Mutex M2 pour avoir accès à une ressource partagée R2
  • le thread T1 demande le Mutex M2. Il est bloqué.
  • le thread T2 demande le Mutex M1. Il est bloqué.

Ici, les threads T1 et T2 s'attendent mutuellement. Ce cas apparaît lorsque des threads ont besoin de deux ressources partagées, la ressource R1 contrôlée par le Mutex M1 et la ressource R2 contrôlée par le Mutex M2. Une solution possible est de demander les deux ressources en même temps à l'aide d'un Mutex unique M. Mais ce n'est pas toujours possible si par exemple cela entraîne une mobilisation longue d'une ressource coûteuse. Une autre solution est qu'un thread ayant M1 et ne pouvant obtenir M2, relâche alors M1 pour éviter l'interblocage.

  • On a un tableau dans lequel des threads viennent déposer des données (les écrivains) et d'autres viennent les lire (les lecteurs).
  • Les écrivains sont égaux entre-eux mais exclusifs : un seul écrivain à la fois peut déposer ses données dans le tableau.
  • Les lecteurs sont égaux entre-eux mais exclusifs : un seul lecteur à la fois peut lire les données déposées dans le tableau.
  • Un lecteur ne peut lire les données du tableau que lorsqu'un écrivain en a déposé dedans et un écrivain ne peut déposer de nouvelles données dans le tableau que lorsque celles qui y sont ont été lues par un lecteur.

On peut distinguer deux ressources partagées :

    • le tableau en écriture : un seul écrivain à la fois doit y avoir accès.
    • le tableau en lecture : un seul lecteur à la fois doit y avoir accès.

et un ordre d'utilisation de ces ressources :

  • un lecteur doit toujours passer après un écrivain.
  • un écrivain doit toujours passer après un lecteur, sauf la 1re fois.

On peut contrôler l'accès à ces deux ressources avec deux barrières de type AutoResetEvent :

  • la barrière peutEcrire contrôlera l'accès des écrivains au tableau.
  • la barrière peutLire contrôlera l'accès des lecteurs au tableau.
  • la barrière peutEcrire sera créée initialement ouverte laissant passer ainsi un 1er écrivain et bloquant tous les autres.
  • la barrière peutLire sera créée initialement fermée bloquant tous les lecteurs.
  • lorsqu'un écrivain aura terminé son travail, il ouvrira la barrière peutLire pour laisser entrer un lecteur.
  • lorsqu'un lecteur aura terminé son travail, il ouvrira la barrière peutEcrire pour laisser entrer un écrivain.

Le programme illustrant cette synchronisation par événements est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Threading;
  3.
  4. namespace Chap8 {
  5.     class Program {
  6.         // utilisation de threads lecteurs et écrivains
  7.         // illustre l'utilisation d'événements de synchronisation
  8.
  9.
  10.         // variables de classe
  11.         static int[] data = new int[3];    // ressource partagée entre threads lecteur et threads écrivain
  12.         static Random objRandom = new Random(DateTime.Now.Second);    // un générateur de nombres aléatoires
  13.         static AutoResetEvent peutLire;    // signale qu'on peut lire le contenu de data
  14.         static AutoResetEvent peutEcrire;    // signale qu'on peut écrire le contenu de data
  15.
  16.         //main
  17.         public static void Main(string[] args) {
  18.
  19.             // le nbre de threads à générer
  20.             const int nbThreads = 2;
  21.
  22.             // initialisation des drapeaux
  23.             peutLire = new AutoResetEvent(false);    // on ne peut pas encore lire
  24.             peutEcrire = new AutoResetEvent(true);    // on peut déjà écrire
  25.
  26.             // création des threads lecteurs
  27.             Thread[] lecteurs = new Thread[nbThreads];
  28.             for (int i = 0; i < nbThreads; i++) {
  29.                 // création
  30.                 lecteurs[i] = new Thread(Lire);
  31.                 lecteurs[i].Name = "L" + i.ToString();
  32.                 // lancement
  33.                 lecteurs[i].Start();
  34.             }
  35.
  36.             // création des threads écrivains
  37.             Thread[] écrivains = new Thread[nbThreads];
  38.             for (int i = 0; i < nbThreads; i++) {
  39.                 // création
  40.                 écrivains[i] = new Thread(Ecrire);
  41.                 écrivains[i].Name = "E" + i.ToString();
  42.                 // lancement
  43.                 écrivains[i].Start();
  44.             }
  45.
  46.             //fin de main
  47.             Console.WriteLine("Fin de Main...");
  48.         }
  49.
  50.         // lire le contenu du tableau
  51.         public static void Lire() {
  52. ...
  53.         }
  54.
  55.         // écrire dans le tableau
  56.         public static void Ecrire() {
  57. ....
  58.         }
  59.     }
  60. }
  • ligne 11 : le tableau data est la ressource partagée entre les threads lecteurs et écrivains. Elle est partagée en lecture par les threads lecteurs, en écriture par les threads écrivains.
  • ligne 13 : l'objet peutLire sert à avertir les threads lecteurs qu'ils peuvent lire le tableau data . Il est mis à vrai par le thread écrivain ayant rempli le tableau data . Il est initialisé à false , ligne 23. Il faut qu'un thread écrivain remplisse d'abord le tableau avant de passer l'événement peutLire à vrai .
  • ligne 14 : l'objet peutEcrire sert à avertir les threads écrivains qu'ils peuvent écrire dans le tableau data . Il est mis à vrai par le thread lecteur ayant exploité la totalité du tableau data . Il est initialisé à true , ligne 24. En effet, le tableau data est libre en écriture.
  • lignes 27-34 : création et lancement des threads lecteurs
  • lignes 37-44 : création et lancement des threads écrivains

La méthode Lire exécutée par les threads lecteurs est la suivante :

Cacher Sélectionnez
  1. public static void Lire() {
  2.             // suivi
  3.             Console.WriteLine("Méthode [Lire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
  4.             // on doit attendre l'autorisation de lecture
  5.             peutLire.WaitOne();
  6.             // lecture tableau
  7.             for (int i = 0; i < data.Length; i++) {
  8.                 //attente 1 s
  9.                 Thread.Sleep(1000);
  10.                 // affichage
  11.                 Console.WriteLine("{0:hh:mm:ss} : Le lecteur {1} a lu le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
  12.             }
  13.             // on peut écrire
  14.             peutEcrire.Set();
  15.             // suivi
  16.             Console.WriteLine("Méthode [Lire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
  17.         }
  • ligne 5 : on attend qu'un thread écrivain signale que le tableau a été rempli. Lorsque ce signal sera reçu, un seul des threads lecteurs en attente de ce signal pourra passer.
  • lignes 7-12 : exploitation du tableau data avec un Sleep au milieu pour forcer le thread à perdre le processeur.
  • ligne 14 : indique aux threads écrivains que le tableau a été lu et qu'il peut être rempli de nouveau.

La méthode Ecrire exécutée par les threads écrivains est la suivante :

Cacher Sélectionnez
  1. public static void Ecrire() {
  2.             // suivi
  3.             Console.WriteLine("Méthode [Ecrire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
  4.             // on doit attendre l'autorisation d'écriture
  5.             peutEcrire.WaitOne();
  6.             // écriture tableau
  7.             for (int i = 0; i < data.Length; i++) {
  8.                 //attente 1 s
  9.                 Thread.Sleep(1000);
  10.                 // affichage
  11.                 data[i] = objRandom.Next(0, 1000);
  12.                 Console.WriteLine("{0:hh:mm:ss} : L'écrivain {1} a écrit le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
  13.             }
  14.             // on peut lire
  15.             peutLire.Set();
  16.             // suivi
  17.             Console.WriteLine("Méthode [Ecrire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
  18.         }
  • ligne 5 : on attend qu'un thread lecteur signale que le tableau a été lu. Lorsque ce signal sera reçu, un seul des threads écrivains en attente de ce signal pourra passer.
  • lignes 7-13 : exploitation du tableau data avec un Sleep au milieu pour forcer le thread à perdre le processeur.
  • ligne 15 : indique aux threads lecteurs que le tableau a été rempli et qu'il peut être lu de nouveau.

L'exécution donne les résultats suivants :

Cacher Sélectionnez
  1. Méthode [Lire] démarrée par le thread n° L0
  2. Méthode [Lire] démarrée par le thread n° L1
  3. Méthode [Ecrire] démarrée par le thread n° E0
  4. Méthode [Ecrire] démarrée par le thread n° E1
  5. Fin de Main...
  6. 02:29:18 : L'écrivain E0 a écrit le nombre 607
  7. 02:29:19 : L'écrivain E0 a écrit le nombre 805
  8. 02:29:20 : L'écrivain E0 a écrit le nombre 650
  9. Méthode [Ecrire] terminée par le thread n° E0
  10. 02:29:21 : Le lecteur L0 a lu le nombre 607
  11. 02:29:22 : Le lecteur L0 a lu le nombre 805
  12. 02:29:23 : Le lecteur L0 a lu le nombre 650
  13. Méthode [Lire] terminée par le thread n° L0
  14. 02:29:24 : L'écrivain E1 a écrit le nombre 186
  15. 02:29:25 : L'écrivain E1 a écrit le nombre 881
  16. 02:29:26 : L'écrivain E1 a écrit le nombre 415
  17. Méthode [Ecrire] terminée par le thread n° E1
  18. 02:29:27 : Le lecteur L1 a lu le nombre 186
  19. 02:29:28 : Le lecteur L1 a lu le nombre 881
  20. 02:29:29 : Le lecteur L1 a lu le nombre 415
  21. Méthode [Lire] terminée par le thread n° L1

On peut remarquer les points suivants :

  • on a bien 1 seul lecteur à la fois, bien que celui-ci perde le processeur dans la section critique Lire
  • on a bien 1 seul écrivain à la fois, bien que celui-ci perde le processeur dans la section critique Ecrire
  • un lecteur ne lit que lorsqu'il y a quelque chose à lire dans le tableau
  • un écrivain n'écrit que lorsque le tableau a été entièrement lu

XI-F-2. La classe Monitor

Dans l'exemple précédent :

  • il y a deux ressources partagées à gérer
  • pour une ressource donnée, les threads sont égaux.

Lorsque les threads écrivains sont bloqués sur l'instruction peutEcrire.WaitOne , l'un d'entre-eux, n'importe lequel, est débloqué par l'opération peutEcrire.Set . Si l'opération précédente doit ouvrir la barrière à un écrivain en particulier, les choses deviennent plus compliquées.

On peut considérer l'analogie avec un établissement accueillant du public à des guichets où chaque guichet est spécialisé. Lorsque le client arrive, il prend un ticket au distributeur de tickets pour le guichet X puis va s'asseoir. Chaque ticket est numéroté et les clients sont appelés par leur numéro via un haut-parleur. Pendant son attente, le client fait ce qu'il veut. Il peut lire ou somnoler. Il est réveillé à chaque fois par le haut-parleur qui annonce que le n° Y est appelé au guichet X. S'il s'agit de lui, le client se lève et accède au guichet X, sinon il continue ce qu'il faisait.

On peut ici fonctionner de façon analogue. Prenons l'exemple des écrivains :

plusieurs écrivains attendent pour un même guichet | leurs threads sont bloqués
le guichet se libère et le n° de l'écrivain suivant est appelé | le thread qui utilisait le tableau en lecture indique aux écrivains que le tableau est disponible. Lui ou un autre thread a fixé le thread écrivain qui doit passer la barrière.
chaque écrivain regarde son n° et seul celui qui a le n° appelé va au guichet. Les autres se remettent en attente. | chaque thread vérifie s'il est l'élu. Si oui, il passe la barrière. Si non, il se remet en attente.

La classe Monitor permet de mettre en œuvre ce scénario.

Image non disponible

Nous décrivons maintenant une construction standard ( pattern ), proposée dans le chapitre Threading du livre C# 3.0 référencé dans l'introduction de ce document, capable de résoudre les problèmes de barrière avec condition d'entrée.

  • Tout d'abord, les threads qui se partagent une ressource (le guichet…) y accèdent via un objet que nous appellerons un jeton. Pour ouvrir la barrière qui mène au guichet, il faut avoir le jeton pour l'ouvrir et il n'y a qu'un seul jeton. Les threads doivent donc se passer le jeton entre-eux.
Cacher Sélectionnez
  1. object jeton=new object();
  • Pour aller au guichet, les threads demandent tout d'abord le jeton :
Cacher Sélectionnez
  1. Monitor.Enter(jeton);

Si le jeton est libre, il est donné au thread ayant exécuté l'opération précédente, sinon le thread est mis en attente du jeton.

  • Si l'accès au guichet se fait de façon non ordonnée, c.a.d. dans le cas où la personne qui entre n'importe pas, l'opération précédente est suffisante. Le thread ayant le jeton va au guichet. Si l'accès se fait de façon ordonnée, le thread qui a le jeton vérifie qu'il remplit la condition pour aller au guichet :
Cacher Sélectionnez
  1. while (! jeNeSuisPasCeluiQuiEstAttendu) {Monitor.Wait(jeton);}

Si le thread n'est pas celui qui est attendu au guichet, il laisse son tour en redonnant le jeton. Il passe dans un état bloqué. Il sera réveillé dès que le jeton redeviendra disponible pour lui. Il vérifiera alors de nouveau s'il vérifie la condition pour aller au guichet. L'opération Monitor.Wait(jeton) qui relâche le jeton ne peut être faite que si le thread est propriétaire du jeton. Si ce n'est pas le cas, une exception est lancée.

  • Le thread qui vérifie la condition pour aller au guichet y va :
Cacher Sélectionnez
  1. // travail au guichet
  2. ....

Avant de quitter le guichet, le thread doit rendre son jeton, sinon les threads bloqués en attente de celui-ci le resteront indéfiniment. Il y a deux situations différentes :

  • la première situation est celle où le thread ayant le jeton est également celui qui signale aux threads en attente du jeton que celui-ci est libre. Il le fera de la façon suivante :
Cacher Sélectionnez
  1. // travail au guichet
  2. ....
  3. // modification condition d'accès au guichet
  4. ...
  5. // réveil des threads en attente du jeton
  6. Monitor.PulseAll(jeton);
  7. // libération du jeton
  8. Monitor.Exit(jeton);

Ligne 6, il réveille les threads en attente du jeton. Ce réveil signifie qu'ils deviennent éligibles pour recevoir le jeton. Cela ne veut pas dire qu'ils le reçoivent immédiatement. Ligne 8, le jeton est libéré. Tous les threads éligibles vont recevoir tour à tour le jeton, de façon indéterministe. Cela va leur donner l'occasion de vérifier de nouveau s'ils vérifient la condition d'accès. Le thread ayant libéré le jeton a modifié cette condition ligne 4 afin de permettre à un nouveau thread d'entrer. Le premier qui la vérifie garde le jeton et va au guichet à son tour.

  • la seconde situation est celle où le thread ayant le jeton n'est pas celui qui doit signaler aux threads en attente du jeton que celui-ci est libre. Il doit néanmoins le libérer parce que le thread chargé d'envoyer ce signal doit être détenteur du jeton. Il le fera par l'opération :
Cacher Sélectionnez
  1. Monitor.Exit(jeton);

Le jeton est désormais disponible, mais les threads qui l'attendent (ils ont fait une opération Wait(jeton)) n'en sont pas avertis. Cette tâche est confiée à un autre thread qui à un moment donné exécutera un code similaire au suivant :

Cacher Sélectionnez
  1. // acquisition jeton
  2. Monitor.Enter(jeton);
  3. // modification condition d'accès au guichet
  4. ....
  5. // réveil des threads en attente du jeton
  6. Monitor.PulseAll(jeton);
  7. // libération du jeton
  8. Monitor.Exit(jeton);

Au final, la construction standard proposée dans le chapitre Threading du livre C# 3.0 est la suivante :

  • définir le jeton d'accès au guichet :
Cacher Sélectionnez
  1. object jeton=new object();
  • demander l'accès au guichet :
Cacher Sélectionnez
  1. lock(jeton){
  2.     while (! jeNeSuisPasCeluiQuiEstAttendu)
  3.         Monitor.Wait(jeton);
  4. }
  5. // passage au guichet
  6. ...
  7.
  8. lock(jeton){...}

est équivalent à

Cacher Sélectionnez
  1. Monitor.Enter(jeton);
  2. try{...} finally{Monitor.Exit(jeton);}

On notera que dans ce schéma le jeton est relâché immédiatement, dès que la barrière est passée. Un autre thread peut alors tester la condition d'accès. La construction précédente laisse donc entrer tous les threads vérifiant la condition d'accès. Si ce n'est pas ce qui est désiré, on pourra écrire :

Cacher Sélectionnez
  1. lock(jeton){
  2.     while (! jeNeSuisPasCeluiQuiEstAttendu)
  3.         Monitor.Wait(jeton);
  4.     // passage au guichet
  5.     ...
  6. }

où le jeton n'est relâché qu'après le passage au guichet.

  • modifier la condition d'accès au guichet et en avertir les autres threads
Cacher Sélectionnez
  1. lock(jeton){
  2.     // modifier la condition d'accès au guichet
  3.     ...
  4.     // en avertir les threads en attente du jeton
  5.     Monitor.PulseAll(jeton);
  6. }

Ci-dessus, la condition d'accès ne peut être modifiée que par le thread ayant le jeton. On pourra aussi écrire :

Cacher Sélectionnez
  1.     // modifier la condition d'accès au guichet
  2.     ...
  3.     // en avertir les threads en attente du jeton
  4.     Monitor.PulseAll(jeton);
  5.     // libérer le jeton
  6.     Monitor.Exit(jeton);

si le thread a déjà le jeton.

Muni de ces informations, nous pouvons réécrire l'application lecteurs / écrivains en fixant un ordre des lecteurs et des écrivains pour l'accès à leurs guichets respectifs. Le code est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Threading;
  3.
  4. namespace Chap8 {
  5.     class Program2 {
  6.         // utilisation de threads lecteurs et écrivains
  7.         // illustre l'utilisation d'événements de synchronisation
  8.
  9.
  10.         // variables de classe
  11.         static int[] data = new int[3];            // ressource partagée entre threads lecteur et threads écrivain
  12.         static Random objRandom = new Random(DateTime.Now.Second);    // un générateur de nombres aléatoires
  13.         static object peutLire = new object();        // signale qu'on peut lire le contenu de data
  14.         static object peutEcrire = new object();    // signale qu'on peut écrire le contenu de data
  15.         static bool lectureAutorisée = false;    // pour autoriser la lecture du tableau
  16.         static bool écritureAutorisée = false;    // pour autoriser l'écriture dans le tableau
  17.         static string[] ordreLecture;    // fixe l'ordre des lecteurs
  18.         static string[] ordreEcriture;    // fixe l'ordre des écrivains
  19.         static int lecteurSuivant = 0;    // indique le n° du lecteur suivant
  20.         static int écrivainSuivant = 0;    // indique le n° de l'écrivain suivant
  21.
  22.         //main
  23.         public static void Main(string[] args) {
  24.
  25.             // le nbre de threads à générer
  26.             const int nbThreads = 5;
  27.
  28.             // création des threads lecteurs
  29.             Thread[] lecteurs = new Thread[nbThreads];
  30.             for (int i = 0; i < nbThreads; i++) {
  31.                 // création
  32.                 lecteurs[i] = new Thread(Lire);
  33.                 lecteurs[i].Name = "L" + i.ToString();
  34.                 // lancement
  35.                 lecteurs[i].Start();
  36.             }
  37.
  38.             // création de l'ordre de lecture
  39.             ordreLecture = new string[nbThreads];
  40.             for (int i = 0; i < nbThreads; i++) {
  41.                 ordreLecture[i] = lecteurs[nbThreads - i - 1].Name;
  42.                 Console.WriteLine("Le lecteur {0} est en position {1}", ordreLecture[i], i);
  43.             }
  44.
  45.             // création des threads écrivains
  46.             Thread[] écrivains = new Thread[nbThreads];
  47.             for (int i = 0; i < nbThreads; i++) {
  48.                 // création
  49.                 écrivains[i] = new Thread(Ecrire);
  50.                 écrivains[i].Name = "E" + i.ToString();
  51.                 // lancement
  52.                 écrivains[i].Start();
  53.             }
  54.
  55.             // création de l'ordre d'écriture
  56.             ordreEcriture = new string[nbThreads];
  57.             for (int i = 0; i < nbThreads; i++) {
  58.                 ordreEcriture[i] = écrivains[i].Name;
  59.                 Console.WriteLine("L'écrivain {0} est en position {1}", ordreEcriture[i], i);
  60.             }
  61.
  62.             // autorisation d'écriture
  63.             lock (peutEcrire) {
  64.                écritureAutorisée = true;
  65.                 Monitor.Pulse(peutEcrire);
  66.             }
  67.
  68.
  69.             //fin de main
  70.             Console.WriteLine("Fin de Main...");
  71.         }
  72.
  73.         // lire le contenu du tableau
  74.         public static void Lire() {
  75. ...
  76.         }
  77.
  78.         // écrire dans le tableau
  79.         public static void Ecrire() {
  80. ...
  81.         }
  82.     }
  83. }

L'accès au guichet de lecture est conditionné par les éléments suivants :

  • ligne 13 : le jeton peutLire
  • ligne 15 : le booléen lectureAutorisée
  • ligne 17 : le tableau ordonné des lecteurs. Les lecteurs vont au guichet de lecture dans l'ordre de ce tableau qui contient leurs noms.
  • ligne 19 : lecteurSuivant indique le n° du prochain lecteur autorisé à aller au guichet.

L'accès au guichet d'écriture est conditionné par les éléments suivants :

  • ligne 14 : le jeton peutEcrire
  • ligne 16 : le booléen écritureAutorisée
  • ligne 18 : le tableau ordonné des écrivains. Les écrivains vont au guichet d'écriture dans l'ordre de ce tableau qui contient leurs noms.
  • ligne 20 : écrivainSuivant indique le n° du prochain écrivain autorisé à aller au guichet.

Les autres éléments du code sont les suivants :

  • lignes 29-36 : création et lancement des threads lecteurs. Ils seront tous bloqués car la lecture n'est pas autorisée (ligne 15).
  • lignes 39-43 : leur ordre de passage au guichet se fera dans l'ordre inverse de leur création.
  • lignes 46-53 : création et lancement des threads érivains. Ils seront tous bloqués car l'écriture n'est pas autorisée (ligne 16).
  • lignes 56-60 : leur ordre de passage au guichet se fera dans l'ordre de leur création.
  • ligne 64 : on autorise l'écriture
  • ligne 65 : on avertit les écrivains que quelque chose a changé.

La méthode Lire est la suivante :

Cacher Sélectionnez
  1.         public static void Lire() {
  2.             // suivi
  3.             Console.WriteLine("Méthode [Lire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
  4.             // on doit attendre l'autorisation de lecture
  5.             lock (peutLire) {
  6.                 while (!lectureAutorisée || ordreLecture[lecteurSuivant] != Thread.CurrentThread.Name) {
  7.                     Monitor.Wait(peutLire);
  8.                 }
  9.                 // lecture tableau
  10.                 for (int i = 0; i < data.Length; i++) {
  11.                     //attente 1 s
  12.                     Thread.Sleep(1000);
  13.                     // affichage
  14.                     Console.WriteLine("{0:hh:mm:ss} : Le lecteur {1} a lu le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
  15.                 }
  16.                 // lecteur suivant
  17.                 lectureAutorisée = false;
  18.                 lecteurSuivant++;
  19.                 // on prévient les écrivains qu'ils peuvent écrire
  20.                 lock (peutEcrire) {
  21.                     écritureAutorisée = true;
  22.                     Monitor.PulseAll(peutEcrire);
  23.                 }
  24.
  25.                 // suivi
  26.                 Console.WriteLine("Méthode [Lire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
  27.             }
  28. }
  • l'ensemble de l'accès au guichet est contrôlé par le lock des lignes 5-27. Le lecteur qui récupère le jeton le garde pendant tout son passage au guichet
  • lignes 6-8 : un lecteur ayant acquis le jeton ligne 5 le relâche si la lecture n'est pas autorisée ou si ce n'est pas à son tour de passer.
  • lignes 10-15 : passage au guichet (exploitation du tableau)
  • lignes 17-18 : le thread change les conditions d'accès au guichet de lecture. On notera qu'il a toujours le jeton de lecture et que ces modifications ne peuvent pas encore permettre à un lecteur de passer.
  • lignes 20-23 : le thread change les conditions d'accès au guichet d'écriture et prévient tous les écrivains en attente que quelque chose a changé.
  • ligne 27 : le lock se termine, le jeton peutLire est relâché. Un thread de lecture pourrait alors l'acquérir ligne 5 mais il ne passerait pas la condition d'accès puisque le booléen lectureAutorisée est à faux. Par ailleurs, tous les threads qui sont en attente du jeton peutLire le restent car l'opération PulseAll ( peutLire ) n'a pas encore eu lieu.

La méthode Ecrire est la suivante :

Cacher Sélectionnez
  1.         public static void Ecrire() {
  2.             // suivi
  3.             Console.WriteLine("Méthode [Ecrire] démarrée par le thread n° {0}", Thread.CurrentThread.Name);
  4.             // on doit attendre l'autorisation d'écriture
  5.             lock (peutEcrire) {
  6.                 while (!écritureAutorisée || ordreEcriture[écrivainSuivant] != Thread.CurrentThread.Name) {
  7.                     Monitor.Wait(peutEcrire);
  8.                 }
  9.                 // écriture tableau
  10.                 for (int i = 0; i < data.Length; i++) {
  11.                     //attente 1 s
  12.                     Thread.Sleep(1000);
  13.                     // affichage
  14.                     data[i] = objRandom.Next(0, 1000);
  15.                     Console.WriteLine("{0:hh:mm:ss} : L'écrivain {1} a écrit le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]);
  16.                 }
  17.                 // écrivain suivant
  18.                 écritureAutorisée = false;
  19.                 écrivainSuivant++;
  20.                 // on réveille les lecteurs en attente du jeton peutLire
  21.                 lock (peutLire) {
  22.                     lectureAutorisée = true;
  23.                     Monitor.PulseAll(peutLire);
  24.                 }
  25.                 // suivi
  26.                 Console.WriteLine("Méthode [Ecrire] terminée par le thread n° {0}", Thread.CurrentThread.Name);
  27.             }
  28. }
  • l'ensemble de l'accès au guichet d'écriture est contrôlé par le lock des lignes 5-27. L'écrivain qui récupère le jeton le garde pendant tout son passage au guichet
  • lignes 6-8 : un écrivain ayant acquis le jeton ligne 5 le relâche si l'écriture n'est pas autorisée ou si ce n'est pas à son tour de passer.
  • lignes 10-16 : passage au guichet (exploitation du tableau)
  • lignes 18-19 : le thread change les conditions d'accès au guichet d'écriture. On notera qu'il a toujours le jeton d'écriture et que ces modifications ne peuvent pas encore permettre à un écrivain de passer.
  • lignes 21-24 : le thread change les conditions d'accès au guichet de lecture et prévient tous les lecteurs en attente que quelque chose a changé.
  • ligne 27 : le lock se termine, le jeton peutEcrire est relâché. Un thread d'écriture pourrait alors l'acquérir ligne 5 mais il ne passerait pas la condition d'accès puisque le booléen écritureAutorisée est à faux. Par ailleurs, tous les threads qui sont en attente du jeton peutEcrire le restent dans l'attente d'une nouvelle opération PulseAll ( peutEcrire ).

Un exemple d'exécution est le suivant :

Cacher Sélectionnez
  1. Méthode [Lire] démarrée par le thread n° L0
  2. Méthode [Lire] démarrée par le thread n° L2
  3. Méthode [Lire] démarrée par le thread n° L1
  4. Le lecteur L2 est en position 0
  5. Le lecteur L1 est en position 1
  6. Le lecteur L0 est en position 2
  7. Méthode [Ecrire] démarrée par le thread n° E0
  8. Méthode [Ecrire] démarrée par le thread n° E1
  9. L'écrivain E0 est en position 0
  10. L'écrivain E1 est en position 1
  11. L'écrivain E2 est en position 2
  12. Fin de Main...
  13. Méthode [Ecrire] démarrée par le thread n° E2
  14. 12:09:05 : L'écrivain E0 a écrit le nombre 815
  15. 12:09:06 : L'écrivain E0 a écrit le nombre 990
  16. 12:09:07 : L'écrivain E0 a écrit le nombre 563
  17. Méthode [Ecrire] terminée par le thread n° E0
  18. 12:09:08 : Le lecteur L2 a lu le nombre 815
  19. 12:09:09 : Le lecteur L2 a lu le nombre 990
  20. 12:09:10 : Le lecteur L2 a lu le nombre 563
  21. Méthode [Lire] terminée par le thread n° L2
  22. 12:09:11 : L'écrivain E1 a écrit le nombre 411
  23. 12:09:12 : L'écrivain E1 a écrit le nombre 11
  24. 12:09:13 : L'écrivain E1 a écrit le nombre 54
  25. Méthode [Ecrire] terminée par le thread n° E1
  26. 12:09:14 : Le lecteur L1 a lu le nombre 411
  27. 12:09:15 : Le lecteur L1 a lu le nombre 11
  28. 12:09:16 : Le lecteur L1 a lu le nombre 54
  29. Méthode [Lire] terminée par le thread n° L1
  30. 12:09:17 : L'écrivain E2 a écrit le nombre 698
  31. 12:09:18 : L'écrivain E2 a écrit le nombre 448
  32. 12:09:19 : L'écrivain E2 a écrit le nombre 472
  33. Méthode [Ecrire] terminée par le thread n° E2
  34. 12:09:20 : Le lecteur L0 a lu le nombre 698
  35. 12:09:21 : Le lecteur L0 a lu le nombre 448
  36. 12:09:22 : Le lecteur L0 a lu le nombre 472
  37. Méthode [Lire] terminée par le thread n° L0

XI-G. Les pools de threads

Jusqu'à maintenant, pour gérer des threads :

  • nous les avons créés par Thread T=new Thread(…)
  • puis exécutés par T.Start()

Nous avons vu au chapitre "Bases de données" qu'avec certains SGBD il était possible d'avoir des pools de connexions ouvertes :

  • n connexions sont ouvertes au démarrage du pool
  • lorsqu'un thread demande une connexion, on lui donne l'une des connexions ouvertes du pool
  • lorsque le thread ferme la connexion, elle n'est pas fermée mais rendue au pool

L'usage d'un pool de connexions est transparent au niveau du code. L'intérêt réside dans l'amélioration des performances : l'ouverture d'une connexion coûte cher. Ici 10 connexions ouvertes peuvent servir des centaines de demandes.

Un système analogue existe pour les threads :

  • min threads sont créés au démarrage du pool. La valeur de min est fixée avec la méthode ThreadPool.SetMinThreads(min1,min2). Un pool de threads peut être utilisé pour exécuter des tâches bloquantes ou non bloquantes dites asynchrones. Le premier paramètre min1 fixe le nombre de threads bloquants, le second min2 le nombre de threads asynchrones. Les valeurs actuelles de ces deux valeurs peuvent être obtenues par ThreadPool.GetMinThreads(out min1,out min2).
  • si ce nombre n'est pas suffisant, le pool va créer d'autres threads pour répondre aux demande jusqu'à la limite de max threads. La valeur de max est fixée avec la méthode ThreadPool.SetMaxThreads(max1,max2) . Les deux paramètres ont la même signification que dans la méthode SetMinThreads . Les valeurs actuelles de ces deux valeurs peuvent être obtenues par ThreadPool.GetMaxThreads(out max1,out max2). Lorsque les max1 threads auront été atteints, les demandes de threads pour tâches bloquantes seront mises en attente d'un thread libre dans le pool.

Un pool de threads offre divers avantages :

  • comme pour le pool de connexions, on économise sur le temps de création des threads : 10 threads peuvent servir des centaines de demandes.
  • on sécurise l'application : en fixant un nombre maximum de threads, on évite l'asphyxie de l'application par des demandes trop nombreuses. Celles-ci seront mises en file d'attente.

Pour donner une tâche à un thread du pool, on utilise l'une des deux méthodes :

  • ThreadPool.QueueWorkItem(WaitCallBack)
  • ThreadPool.QueueWorkItem(WaitCallBack,object)

où WaitCallBack est toute méthode ayant la signature void WaitCallBack(object) . La méthode 1 demande à un thread d'exécuter la méthode WaitCallBack sans lui passer de paramètre. La méthode 2 fait la même chose mais en passant un paramètre de type object à la méthode WaitCallBack .

Voici un programme illustrant ces concepts :

Cacher Sélectionnez
  1. using System;
  2. using System.Threading;
  3.
  4. namespace Chap8 {
  5.     class Program {
  6.         public static void Main() {
  7.             // init Thread courant
  8.             Thread main = Thread.CurrentThread;
  9.             // on fixe un nom au Thread
  10.             main.Name = "Main";
  11.
  12.             // on utilise un pool de threads
  13.             int min1, min2;
  14.             // on fixe le nombre minimal de threads bloquants
  15.             ThreadPool.GetMinThreads(out min1, out min2);
  16.             Console.WriteLine("Nombre minimum de tâches bloquantes dans le pool : {0}", min1);
  17.             Console.WriteLine("Nombre minimum de tâches asynchrones dans le pool : {0}", min2);
  18.             ThreadPool.SetMinThreads(3, min2);
  19.             ThreadPool.GetMinThreads(out min1, out min2);
  20.             Console.WriteLine("Nombre minimum de tâches bloquantes dans le pool : {0}", min1);
  21.             // on fixe le nombre maximal de threads bloquants
  22.             int max1, max2;
  23.             ThreadPool.GetMaxThreads(out max1, out max2);
  24.             Console.WriteLine("Nombre maximum de tâches bloquantes dans le pool : {0}", max1);
  25.             Console.WriteLine("Nombre maximum de tâches asynchrones dans le pool : {0}", max2);
  26.             ThreadPool.SetMaxThreads(5, max2);
  27.             ThreadPool.GetMaxThreads(out max1, out max2);
  28.             Console.WriteLine("Nombre maximum de tâches bloquantes dans le pool : {0}", max1);
  29.             // on exécute 7 threads
  30.             for (int i = 0; i < 7; i++) {
  31.                 // on lance l'exécution du thread i dans un pool
  32.                 ThreadPool.QueueUserWorkItem(Sleep, new Data2 { Numéro = i.ToString(), Début = DateTime.Now, Durée = i + 1 });
  33.             }
  34.             // fin de main
  35.             Console.Write("Tapez [entrée] pour terminer le thread {0} à {1:hh:mm:ss}", main.Name, DateTime.Now);
  36.             // attente
  37.             Console.ReadLine();
  38.         }
  39.
  40.         public static void Sleep(object infos) {
  41.             // on récupère le paramètre
  42.             Data2 data = infos as Data2;
  43.             Console.WriteLine("Le thread n° {0} va dormir pendant {1} seconde(s)", data.Numéro, data.Durée);
  44.             // état du pool
  45.             int cpt1, cpt2;
  46.             ThreadPool.GetAvailableThreads(out cpt1, out cpt2);
  47.             Console.WriteLine("Nombre de threads pour tâches bloquantes disponibles dans le pool : {0}", cpt1);
  48.             // mise en sommeil pendant Durée secondes
  49.             Thread.Sleep(data.Durée * 1000);
  50.             // fin d'exécution
  51.             data.Fin = DateTime.Now;
  52.             Console.WriteLine("Le thread n° {0} est terminé. Il était programmé pour durer {1} seconde(s). Il a duré {2} seconde(s)", data.Numéro, data.Durée, data.Fin - data.Début);
  53.         }
  54.     }
  55.
  56.     internal class Data2 {
  57.         // informations diverses
  58.         public string Numéro { get; set; }
  59.         public DateTime Début { get; set; }
  60.         public int Durée { get; set; }
  61.         public DateTime Fin { get; set; }
  62.     }
  63. }
  • ligne 15-17 : on demande et affiche le nombre minimal actuel des deux types de threads du pool de threads
  • ligne 18 : on change le nombre minimal de threads pour tâches bloquantes : 2
  • lignes 19-21 : on affiches les nouveaux minima
  • lignes 22-28 : on fait de même pour fixer le nombre maximal de threads pour tâches bloquantes : 5
  • lignes 30-33 : on fait exécuter 7 tâches dans un pool de 5 threads. 5 tâches devraient obtenir 1 thread, les 2 premières rapidement puisque 2 threads sont toujours présents, les 3 autres avec un délai d'attente de 0.5 seconde. 2 tâches devraient attendre qu'un thread se libère.
  • ligne 32 : les tâches exécutent la méthode Sleep des lignes 40-54 en lui passant un paramètre de type Data2 défini lignes 56-62.
  • ligne 40 : la méthode Sleep exécutée par les tâches
  • ligne 42 : on récupère le paramètre passé à la méthode Sleep .
  • ligne 43 : la tâche s'identifie sur la console
  • lignes 45-47 : on affiche le nombre de threads actuellement disponibles. On veut voir comment il évolue.
  • ligne 49 : la tâche s'arrête quelques secondes (tâche bloquante).
  • ligne 52 : lorsqu'elle se réveille, on fait afficher quelques informations sur son compte.

Les résultats obtenus sont les suivants.

Pour les nombres min et max de threads dans le pool :

Cacher Sélectionnez
  1. Nombre minimum de tâches bloquantes dans le pool : 2
  2. Nombre minimum de tâches asynchrones dans le pool : 2
  3. Nombre minimum de tâches bloquantes dans le pool après changement : 3
  4. Nombre maximum de tâches bloquantes dans le pool : 500
  5. Nombre maximum de tâches asynchrones dans le pool : 1000
  6. Nombre maximum de tâches bloquantes dans le pool après changement : 5

Pour l'exécution des 7 threads :

Cacher Sélectionnez
  1. A 03:07:37:04, le thread n° 0 va dormir pendant 10 seconde(s)
  2. Nombre de threads pour tâches bloquantes disponibles dans le pool : 3
  3. A 03:07:37:04, le thread n° 2 va dormir pendant 12 seconde(s)
  4. Nombre de threads pour tâches bloquantes disponibles dans le pool : 2
  5. A 03:07:37:04, le thread n° 1 va dormir pendant 11 seconde(s)
  6. Nombre de threads pour tâches bloquantes disponibles dans le pool : 2
  7. A 03:07:38:04, le thread n° 3 va dormir pendant 13 seconde(s)
  8. Nombre de threads pour tâches bloquantes disponibles dans le pool : 1
  9. A 03:07:38:54, le thread n° 4 va dormir pendant 14 seconde(s)
  10. Nombre de threads pour tâches bloquantes disponibles dans le pool : 0
  11. A 03:07:47:04, le thread n° 0 se termine. Il était programmé pour durer 10 seconde(s). Il a duré 00:00:10 seconde(s)
  12. A 03:07:47:04, le thread n° 5 va dormir pendant 15 seconde(s)
  13. Nombre de threads pour tâches bloquantes disponibles dans le pool : 0
  14. A 03:07:48:04, le thread n° 1 se termine. Il était programmé pour durer 11 seconde(s). Il a duré 00:00:11 seconde(s)
  15. A 03:07:48:04, le thread n° 6 va dormir pendant 16 seconde(s)
  16. Nombre de threads pour tâches bloquantes disponibles dans le pool : 0
  17. A 03:07:49:04, le thread n° 2 se termine. Il était programmé pour durer 12 seconde(s). Il a duré 00:00:12 seconde(s)
  18. A 03:07:51:04, le thread n° 3 se termine. Il était programmé pour durer 13 seconde(s). Il a duré 00:00:14 seconde(s)
  19. A 03:07:52:54, le thread n° 4 se termine. Il était programmé pour durer 14 seconde(s). Il a duré 00:00:15.5000000 seconde(s)
  20. A 03:08:02:04, le thread n° 5 se termine. Il était programmé pour durer 15 seconde(s). Il a duré 00:00:25 seconde(s)
  21. A 03:08:04:04, le thread n° 6 se termine. Il était programmé pour durer 16 seconde(s). Il a duré 00:00:27 seconde(s)
  • lignes 1-6 : les 3 premières tâches sont exécutées tour à tour. Elles trouvent imédiatement 1 thread disponible ( MinThreads =3) puis se mette en sommeil.
  • lignes 7-9 : pour les tâches 3 et 4, c'est un peu plus long. Pour chacun d'eux il n'y avait pas de thread libre. Il a fallu en créer un. Ce mécanisme est possible jusqu'à 5 (MaxThreads=5).
  • ligne 10 : il n'y a plus de threads disponibles : les tâches 5 et 6 vont devoir attendre.
  • lignes 11-12 : la tâche 0 se termine. La tâche 5 prend son thread.
  • lignes 13-14 : la tâche 1 se termine. La tâche 6 prend son thread.
  • lignes 17-21 : les tâches se terminent les unes après les autres.

XI-H. La classe BackgroundWorker

XI-H-1. Exemple 1

La classe BackgroundWorker appartient à l'espace de noms [System.ComponentModel]. Elle s'utilise comme un thread mais présente des particularités qui peuvent la rendre, dans certains cas, plus intéressante que la classe [Thread] :

  • elle émet les événements suivants :
  • DoWork : un thread a demandé l'exécution du BackgroundWorker
  • ProgressChanged : l'objet BackgroundWorker a exécuté la méthode ReportProgress . Celle-ci sert à donner un pourcentage d'exécution.
  • RunWorkerCompleted : l'objet BackgroundWorker a terminé son travail. Il a pu le terminer normalement ou sur annulation ou exception.

Ces événements rendent le BackgroundWorker utile dans les interfaces graphiques : une tâche longue sera confiée à un BackgroundWorker qui pourra rendre compte de son avancement avec l'événement ProgressChanged et sa fin avec l'événement RunWorkerCompleted . Le travail à effectuer par le BackgroundWorker sera gfait par une méthode qui aura été associée à l'événement DoWork .

  • il est possible de demander son annulation. Dans une interface graphique, une tâche longue pourra ainsi être annulée par l'utilisateur.
  • les objets BackgroundWorker appartiennent à un pool et sont recyclés selon les besoins. Une application qui a besoin d'un objet BackgroundWorker l'obtiendra auprès du pool qui lui donnera un thread déjà existant mais inutilisé. Le fait de recycler ainsi les threads plutôt que de créer à chaque fois un thread neuf, améliore les performances.

Nous utilisons cet outil sur l'application précédente dans le cas où l'accès au guichet est non contrôlé :

Cacher Sélectionnez
  1. using System;
  2. using System.Threading;
  3. using System.ComponentModel;
  4.
  5. namespace Chap8 {
  6.     class Program2 {
  7.         // utilisation de threads lecteurs et écrivains
  8.         // illustre l'utilisation simultanée de ressources partagées et de synchronisation
  9.
  10.         // variables de classe
  11.         const int nbThreads = 2;                    // nbre de threads au total
  12.         static int nbLecteursTerminés = 0;        // nbre de threads terminés
  13.         static int[] data = new int[5];            // tableau partagé entre threads lecteur et threads écrivain
  14.         static object appli;                            // synchronise l'accès au nbre de threads terminés
  15.         static Random objRandom = new Random(DateTime.Now.Second);    // un générateur de nombres aléatoires
  16.         static AutoResetEvent peutLire;        // signale qu'on peut lire le contenu du tableau
  17.         static AutoResetEvent peutEcrire;        // signale qu'on peut écrire dans le tableau
  18.         static AutoResetEvent finLecteurs;    // signale la fin des lecteurs
  19.
  20.         //main
  21.         public static void Main(string[] args) {
  22.
  23.             // on donne un nom au thread
  24.             Thread.CurrentThread.Name = "Main";
  25.
  26.             // initialisation des drapeaux
  27.             peutLire = new AutoResetEvent(false);        // on ne peut pas encore lire
  28.             peutEcrire = new AutoResetEvent(true);    // on peut déjà écrire
  29.             finLecteurs = new AutoResetEvent(false);    // appli non terminée
  30.
  31.             // synchronise l'accès au compteur de threads terminés
  32.             appli = new object();
  33.
  34.             // création des threads lecteurs
  35.             MyBackgroundWorker[] lecteurs = new MyBackgroundWorker[nbThreads];
  36.             for (int i = 0; i < nbThreads; i++) {
  37.                 // création
  38.                 lecteurs[i] = new MyBackgroundWorker();
  39.                 lecteurs[i].Numéro = "L" + i;
  40.                 lecteurs[i].DoWork += Lire;
  41.                 lecteurs[i].RunWorkerCompleted += EndLecteur;
  42.                 // lancement
  43.                 lecteurs[i].RunWorkerAsync();
  44.             }
  45.
  46.             // création des threads écrivains
  47.             MyBackgroundWorker[] écrivains = new MyBackgroundWorker[nbThreads];
  48.             for (int i = 0; i < nbThreads; i++) {
  49.                 // création
  50.                 écrivains[i] = new MyBackgroundWorker();
  51.                 écrivains[i].Numéro = "E" + i;
  52.                 écrivains[i].DoWork += Ecrire;
  53.                 // lancement
  54.                 écrivains[i].RunWorkerAsync();
  55.             }
  56.
  57.             // attente de la fin de tous les threads
  58.             finLecteurs.WaitOne();
  59.             //fin de main
  60.             Console.WriteLine("Fin de Main...");
  61.         }
  62.
  63.         public static void EndLecteur(object sender, RunWorkerCompletedEventArgs infos) {
  64. ...
  65.         }
  66.
  67.         // lire le contenu du tableau
  68.         public static void Lire(object sender, DoWorkEventArgs infos) {
  69. ...
  70.         }
  71.
  72.         // écrire dans le tableau
  73.         public static void Ecrire(object sender, DoWorkEventArgs infos) {
  74. ...
  75.         }
  76.     }
  77.
  78.     // thread
  79.     internal class MyBackgroundWorker : BackgroundWorker {
  80.         // informations diverses
  81.         public string Numéro { get; set; }
  82.     }
  83.
  84. }

Nous ne détaillons que les changements :

  • la classe Thread est remplacée par la classe MyBackgroundWorker des lignes 79-82. La classe BackgroundWorker a été dérivée afin de donner un numéro au thread. On aurait pu procéder différemment en passant un objet à la méthode RunWorkerAsync des lignes 43 et 54, objet contenant le n° du thread.
  • ligne 58 : la méthode Main se termine après que tous les threads lecteurs ont fait leur travail. Pour cela, ligne 12, le compteur nbLecteursTerminés compte le nombre de threads lecteurs ayant terminé leur travail. Ce compteur est incrémenté par la méthode EndLecteur des lignes 63-65 qui est exécutée à chaque fois qu'un thread lecteur se termine. C'est cette procédure qui contrôle l'événement AutoResetEvent finLecteurs de la ligne 18 sur lequel se synchronise, ligne 59, la méthode Main.
  • ligne 16 : parce que plusieurs threads lecteurs peuvent vouloir incrémenter en même temps le compteur nbLecteursTerminés , un accès exclusif à celui-ci est assuré par l'objet de synchronisation appli . Ce cas est improbable mais théoriquement possible.
  • lignes 35-44 : création des threads lecteurs
  • ligne 38 : création du thread de type MyBackgroundWorker
  • ligne 39 : on lui donne un N°
  • ligne 40 : on lui assigne la méthode Lire à exécuter
  • ligne 41 : la méthode EndLecteur sera exécutée après la fin du thread
  • ligne 43 : le thread est lancé
  • lignes 47-55 : création des threads écrivains
  • ligne 50 : création du thread de type MyBackgroundWorker
  • ligne 51 : on lui donne un N°
  • ligne 52 : on lui assigne la méthode Ecrire à exécuter
  • ligne 54 : le thread est lancé

Les méthodes Lire et Ecrire restent inchangées. La méthode EndLecteur est exécutée à la fin de chaque thread lecteur. Son code est le suivant :

Cacher Sélectionnez
  1.         public static void EndLecteur(object sender, RunWorkerCompletedEventArgs infos) {
  2.             // incrémentation nbre de lecteurs terminés
  3.             lock (appli) {
  4.                 nbLecteursTerminés++;
  5.                 if (nbLecteursTerminés == nbThreads)
  6.                     finLecteurs.Set();
  7.             }
  8. }

Le rôle de la méthode EndLecteur est d'avertir la méthode Main que tous les lecteurs ont fait leur travail.

  • ligne 4 : le compteur nbLecteursTerminés est incrémenté.
  • lignes 5-6 : si tous les lecteurs ont fait leur travail, alors l'événement finLecteurs est positionné à vrai afin de prévenir la méthode Main qui attend cet événement.
  • parce que la procédure EndLecteur est exécutée par plusieurs threads, la section critique précédente est protégée par la clause lock de la ligne 3.

L'exécution donne des résultats analogues à ceux de la version utilisant des threads.

XI-H-2. Exemple 2

Le code suivant illustre d'autres points de la classe BackgroundWorker :

  • la possibilité d'annuler la tâche
  • la remontée d'une exception lancée dans la tâche
  • le passage d'un paramètre d'E/S à la tâche
Cacher Sélectionnez
  1. using System;
  2. using System.Threading;
  3. using System.ComponentModel;
  4.
  5. namespace Chap8 {
  6.     class Program3 {
  7.
  8.         // threads
  9.         static BackgroundWorker[] tâches = new BackgroundWorker[5];
  10.
  11.         public static void Main() {
  12.             // init Thread courant
  13.             Thread main = Thread.CurrentThread;
  14.             // on fixe un nom au Thread
  15.             main.Name = "Main";
  16.
  17.             // création de threads
  18.             for (int i = 0; i < tâches.Length; i++) {
  19.                 // on crée le thread n° i
  20.                 tâches[i] = new BackgroundWorker();
  21.                 // on l'initialise
  22.                 tâches[i].DoWork += Sleep;
  23.                 tâches[i].RunWorkerCompleted += End;
  24.                 tâches[i].WorkerSupportsCancellation = true;
  25.                 // on le lance
  26.                 tâches[i].RunWorkerAsync(new Data { Numéro = i, Début = DateTime.Now, Durée = i + 1 });
  27.             }
  28.             // on annule le dernier thread
  29.             tâches[4].CancelAsync();
  30.
  31.             // fin de main
  32.             Console.WriteLine("Fin du thread {0}, tapez [entrée] pour terminer...", main.Name);
  33.             Console.ReadLine();
  34.             return;
  35.         }
  36.
  37.         public static void Sleep(object sender, DoWorkEventArgs infos) {
  38. ...
  39.         }
  40.
  41.         public static void End(object sender, RunWorkerCompletedEventArgs infos) {
  42. ...
  43.         }
  44.
  45.         internal class Data {
  46.             // informations diverses
  47.             public int Numéro { get; set; }
  48.             public DateTime Début { get; set; }
  49.             public int Durée { get; set; }
  50.             public DateTime Fin { get; set; }
  51.         }
  52.     }
  53. }
  • ligne 9 : le tableau de BackgroundWorker
  • lignes 18-27 : création des threads
  • ligne 20 : création du thread
  • ligne 22 : le thread exécutera la méthode Sleep des lignes 39-41
  • ligne 23 : la méthode End des lignes 43-45 sera exécutée à la fin du thread
  • ligne 24 : le thread pourra être annulé
  • ligne 26 : le thread est lancé avec un paramètre de type [Data], défini lignes 49-52. Cet objet a les champs suivants :
  • Numéro (entrée) : n° du thread
  • Début (entrée) : heure de début d'exécution du thread
  • Durée (entrée) : durée d'exécution du Sleep
  • Fin (sortie) : fin d'exécution du thread
  • ligne 29 : le thread n° 4 est annulé

Tous les threads exécutent la méthode Sleep suivante :

Cacher Sélectionnez
  1.         public static void Sleep(object sender, DoWorkEventArgs infos) {
  2.             // on exploite le paramètre infos
  3.             Data data = (Data)infos.Argument;
  4.             // exception pour la tâche n° 3
  5.             if (data.Numéro == 3) {
  6.                 throw new Exception("test....");
  7.             }
  8.             // mise en sommeil pendant Durée secondes avec un arrêt ttes les secondes
  9.             for (int i = 1; i <= data.Durée && !tâches[data.Numéro].CancellationPending; i++) {
  10.                 // attente d'1 seconde
  11.                 Thread.Sleep(1000);
  12.             }
  13.             // fin d'exécution
  14.             data.Fin = DateTime.Now;
  15.             // on initialise le résultat
  16.             infos.Result = data;
  17.             infos.Cancel = tâches[data.Numéro].CancellationPending;
  18. }
  • ligne 1 : la méthode Sleep a la signature standard des gestionnaires d'événements. Elle reçoit deux paramètres :
  • sender : l'émetteur de l'événement, ici le BackgroundWorker qui exécute la méthode
  • infos : de type DoWorkEventArgs qui donne des informations sur l'événement DoWork . Ce paramètre sert aussi bien à transmettre des informations au thread qu'à récupérer ses résultats.
  • ligne 3 : le paramètre passé à la méthode RunWorkerAsync de la tâche est retrouvé dans la propriété infos.Argument .
  • lignes 5-7 : on lance une exception pour la tâche n° 3
  • lignes 9-12 : le thread "dort" Durée secondes par tranches d'une seconde afin de permettre le test d'annulation de la ligne 9. Cela simule un travail de longue durée au cours duquel le thread vérifierait régulièrement s'il existe une demande d'annulation. Pour indiquer qu'il a été annulé, le thread doit mettre la propriété infos.Cancel à vrai (ligne 17).
  • ligne 16 : le thread peut rendre un résultat au thread qui l'a lancé. Il place ce résultat dans infos.Result .

Une fois terminés, les threads exécutent la méthode End suivante :

Cacher Sélectionnez
  1. public static void End(object sender, RunWorkerCompletedEventArgs infos) {
  2.             // on exploite le paramètre infos pour afficher le résultat de l'exécution
  3.             // exception ?
  4.             if (infos.Error != null) {
  5.                 Console.WriteLine("Le thread {1} a rencontré l'erreur suivante : {0}", infos.Error.Message, sender);
  6.             } else
  7.                 if (!infos.Cancelled) {
  8.                     Data data = (Data)infos.Result;
  9.                     Console.WriteLine("Thread {0} terminé : début {1:hh:mm:ss}, durée programmée {2} s, fin {3:hh:mm:ss}, durée effective {4}",
  10.                     data.Numéro, data.Début, data.Durée, data.Fin, (data.Fin - data.Début));
  11.                 } else {
  12.                     Console.WriteLine("Thread {0} annulé", sender);
  13.                 }
  14.         }
  • ligne 1 : la méthode End a la signature standard des gestionnaires d'événements. Elle reçoit deux paramètres :
  • sender : l'émetteur de l'événement, ici le BackgroundWorker qui exécute la méthode
  • infos : de type RunWorkerCompletedEventArgs qui donne des informations sur l'événement RunWorkerCompleted .
  • ligne 4 : le champ infos.Error de type Exception est renseigné seulement si une exception s'est produite.
  • ligne 7 : le champ infos.Cancelled de type booléen à la valeur true si le thread a été annulé.
  • ligne 8 : s'il y a pas eu exception ou annulation, alors infos.Result est le résultat du thread exécuté. Utiliser ce résultat s'il y a eu annulation du thread ou si le thread a lancé une exception, provoque une exception. Ainsi lignes 5 et 13, on n'est pas capables d'afficher le n° du thread annulé ou qui a lancé une exception car ce n° est dans infos.Result . Ce problème peut être contourné en dérivant la classe BackgroundWorker pour y mettre les informations à échanger entre le thread appelant et le thread appelé comme il a été fait dans l'exemple précédent. On utilise alors l'argument sender qui représente le BackgroundWorker au lieu de l'argument infos .

Les résultats d'exécution sont les suivants :

Cacher Sélectionnez
  1. Fin du thread Main. Laissez les autres threads se terminer puis tapez [entrée] pour terminer...
  2. Thread 0 terminé : début 05:19:46, durée programmée 1 s, fin 05:19:47, durée effective 00:00:01
  3. Le thread System.ComponentModel.BackgroundWorker a rencontré l'erreur suivante : test....
  4. Thread System.ComponentModel.BackgroundWorker annulé
  5. Thread 1 terminé : début 05:19:46, durée programmée 2 s, fin 05:19:49, durée effective 00:00:03
  6. Thread 2 terminé : début 05:19:46, durée programmée 3 s, fin 05:19:50, durée effective 00:00:04

XI-I. Données locales à un thread

XI-I-1. Le principe

Considérons une application à trois couches :

Image non disponible

Supposons que l'application soit multiutilisateurs, une application web par exemple. Chaque utilisateur est servi par un thread qui lui est dédié. La vie du thread est la suivante :

  • le thread est créé ou demandé à un pool de threads pour satisfaire une demande d'un utilisateur
  • si cette demande nécessite des données, le thread va exécuter une méthode de la couche [ui] qui va appeler une méthode de la couche [metier] qui va à son tour appeler une méthode de la couche [dao].
  • le thread rend la réponse à l'utilisateur. Il disparaît ensuite ou il est recyclé dans un pool de threads.

Dans l'opération 2, il peut être intéressant que le thread ait des données qui lui soient propres, c.a.d. non partagées avec les autres threads. Ces données pourraient par exemple appartenir à l'utilisateur particulier que le thread sert. Ces données pourraient alors être utilisées dans les différentes couches [ui, metier, dao].

La classe Thread permet ce scénario grâce à une sorte de dictionnaire privé où les clés seraient de type LocalDataStoreSlot :

Image non disponible

Un modèle d'utilisation pourrait être le suivant :

  • pour créer un couple ( clé , valeur ) associé au thread courant :
Cacher Sélectionnez
  1. Thread.SetData(Thread.GetNamedDataSlot("clé"),valeur);
  • pour récupérer la valeur associée à clé :
Cacher Sélectionnez
  1. Thread.GetData(Thread.GetNamedDataSlot("clé"));

XI-I-2. Application du principe

Considérons l'application à trois couches suivantes :

Image non disponible

Supposons que la couche [dao] gère une base d'articles et que son interface soit initialement la suivante :

Cacher Sélectionnez
  1. using System.Collections.Generic;
  2.
  3. namespace Chap8 {
  4.     public interface IDao {
  5.         int InsertArticle(Article article);
  6.         List<Article> GetAllArticles();
  7.         void DeleteAllArticles();
  8.     }
  9. }
  • ligne 5 : pour insérer un article dans la base
  • ligne 6 : pour récupérer tous les articles de la base
  • ligne 7 : pour supprimer tous les articles de la base

Ultérieurement, apparaît le besoin d'une méthode pour insérer un tableau d'articles à l'aide d'une transaction parce qu'on souhaite fonctionner en tout ou rien : soit tous les articles sont insérés soit aucun. On peut alors modifier l'interface pour intégrer ce nouveau besoin :

Cacher Sélectionnez
  1. using System.Collections.Generic;
  2.
  3. namespace Chap8 {
  4.     public interface IDao {
  5.         int InsertArticle(Article article);
  6.         void insertArticles(Article[] articles);
  7.         List<Article> GetAllArticles();
  8.         void DeleteAllArticles();
  9.     }
  10. }
  • ligne 6 : pour ajouter un tableau d'articles dans la base

Ultérieurement, pour une autre application, apparaît le besoin de supprimer une liste d'articles enregistrée dans une liste, toujours dans une transaction. On voit que pour répondre à des besoins métier différents, la couche [dao] va être amenée à grossir. On peut prendre une autre voie :

  • ne mettre dans la couche [dao] que les opérations basiques InsertArticle , DeleteArticle , UpdateArticle , SelectArticle , SelectArticles
  • déporter dans la couche [métier] les opérations de mise à jour simultanée de plusieurs articles. Celles-ci utiliseraient les opérations élémentaires de la couche [dao].

L'avantage de cette solution est que la même couche [dao] pourrait être utilisée sans changement avec différentes couches [metier]. Elle amène une difficulté dans la gestion de la transaction qui regroupe des mises à jour à faire de façon atomique sur la base :

  • la transaction doit être initiée par la couche [metier] avant qu'elle n'appelle les méthodes de la couche [dao]
  • les méthodes de la couche [dao] doivent connaître l'existence de la transaction afin d'y prendre part si elle existe
  • la transaction doit êter terminée par la couche [métier].

Pour que les méthodes de la couche [dao] connaissent l'existence d'une éventuelle transaction en cours, on pourrait ajouter la transaction comme paramètre de chaque méthode de la couche [dao]. Ce paramètre va alors apparaître dans la signature des méthodes de l'interface, ce qui va lier celle-ci à une source de données particulière : la base de données. Les données locales du thread nous apportent une solution plus élégante : la couche [métier] mettra la transaction dans les données locales du thread et c'est là que la couche [dao] ira la chercher. La signature des méthodes de la couche [dao] n'a alors pas besoin d'être changée.

Nous mettons en œuvre cette solution avec le projet Visual studio suivant :

Image non disponible
Image non disponible
  • en [1] : la solution dans son ensemble
  • en [2] : les références utilisées. La base [4] étant une base SQL Server Compact, il est nécessaire d'avoir la référence [System.Data.SqlServerCe].
  • en [3] : les différentes couches de l'application.

La base [4] est la base SQL Server Compact déjà utilisée dans le chapitre précédent notamment au paragraphe , page .

Image non disponible

La classe Article

Une ligne de la table [articles] précédente est encapsulée dans un objet de type Article :

Cacher Sélectionnez
  1. namespace Chap8 {
  2.     public class Article {
  3.         // propriétés
  4.         public int Id { get; set; }
  5.         public string Nom { get; set; }
  6.         public decimal Prix { get; set; }
  7.         public int StockActuel { get; set; }
  8.         public int StockMinimum { get; set; }
  9.
  10.         // constructeurs
  11.         public Article() {
  12.         }
  13.
  14.         public Article(int id, string nom, decimal prix, int stockActuel, int stockMinimum) {
  15.             Id = id;
  16.             Nom = nom;
  17.             Prix = prix;
  18.             StockActuel = stockActuel;
  19.             StockMinimum = stockMinimum;
  20.         }
  21.
  22.         // identité
  23.         public override string ToString() {
  24.             return string.Format("[{0},{1},{2},{3},{4}]", Id, Nom, Prix, StockActuel, StockMinimum);
  25.         }
  26.     }
  27. }

Interface de la couche [dao]

L'interface IDao de la couche [dao] sera la suivante :

Cacher Sélectionnez
  1. using System.Collections.Generic;
  2.
  3. namespace Chap8 {
  4.     public interface IDao {
  5.         int InsertArticle(Article article);
  6.         List<Article> GetAllArticles();
  7.         void DeleteAllArticles();
  8.     }
  9. }
  • ligne 5 : pour insérer un article dans la table [articles]
  • ligne 6 : pour mettre toutes les lignes de la table [articles] dans une liste d'objets Article
  • ligne 7 : pour supprimer toutes les lignes de la table [articles]

Interface de la couche [metier]

L'interface IMetier de la couche [metier] sera la suivante :

Cacher Sélectionnez
  1. using System.Collections.Generic;
  2.
  3. namespace Chap8 {
  4.     interface IMetier {
  5.         void InsertArticlesInTransaction(Article[] articles);
  6.         void InsertArticlesOutOfTransaction(Article[] articles);
  7.         List<Article> GetAllArticles();
  8.         void DeleteAllArticles();
  9.     }
  10. }
  • ligne 5 : pour insérer, à l'intérieur d'une transaction, un ensemble d'articles
  • ligne 6 : idem mais sans transaction
  • ligne 7 : pour obtenir la liste de tous les articles
  • ligne 8 : pour supprimer tous les articles

Implémentation de la couche [metier]

L'implémentation Metier de l'interface IMetier sera la suivante :

Cacher Sélectionnez
  1. using System.Collections.Generic;
  2. using System.Data;
  3. using System.Data.SqlServerCe;
  4. using System.Threading;
  5.
  6. namespace Chap8 {
  7.     public class Metier : IMetier {
  8.         // couche [dao]
  9.         public IDao Dao { get; set; }
  10.         // chaîne de connexion
  11.         public string ConnectionString { get; set; }
  12.
  13.         // insertion d'un tableau d'articles à l'intérieur d'une transaction
  14.         public void InsertArticlesInTransaction(Article[] articles) {
  15.             // on crée la connexion à la base
  16.             using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) {
  17.                 // ouverture connexion
  18.                 connexion.Open();
  19.                 // transaction
  20.                 SqlCeTransaction transaction = null;
  21.                 try {
  22.                     // début transaction
  23.                     transaction = connexion.BeginTransaction(IsolationLevel.ReadCommitted);
  24.                     // on enregistre la transaction dans le thread
  25.                     Thread.SetData(Thread.GetNamedDataSlot("transaction"), transaction);
  26.                     // insertion des articles
  27.                     foreach (Article article in articles) {
  28.                         Dao.InsertArticle(article);
  29.                     }
  30.                     // on valide la transaction
  31.                     transaction.Commit();
  32.                 } catch {
  33.                     // on défait la transaction
  34.                     if (transaction != null)
  35.                         transaction.Rollback();
  36.                 }
  37.             }
  38.         }
  39.
  40.         // insertion d'un tableau d'articles sans transaction
  41.         public void InsertArticlesOutOfTransaction(Article[] articles) {
  42.             // insertion des articles
  43.             foreach (Article article in articles) {
  44.                 Dao.InsertArticle(article);
  45.             }
  46.         }
  47.
  48.         // liste des articles
  49.         public List<Article> GetAllArticles() {
  50.             return Dao.GetAllArticles();
  51.         }
  52.         // supprimer tous les articles
  53.         public void DeleteAllArticles() {
  54.             Dao.DeleteAllArticles();
  55.         }
  56.     }
  57. }

La classe a les propriétés suivantes :

  • ligne 9 : une référence sur la couche [dao]
  • ligne 11 : la chaîne de connexion qui permet de se connecter à la base de données des articles

Nous ne commentons que la méthode InsertArticlesInTransaction qui seule présente des difficultés :

  • ligne 16 : une connexion avec la base est créée
  • ligne 18 : elle est ouverte
  • ligne 23 : une transaction est créée
  • ligne 25 : elle est enregistrée dans les données locales du thread, associée à la clé "transaction"
  • lignes 27-29 : la méthode d'insertion unitaire de la couche [dao] est appelée pour chaque article à insérer
  • lignes 21 et 32 : l'ensemble de l'insertion du tableau est contrôlée par un try / catch
  • ligne 31 : si on arrive là, c'est qu'il n'y a pas eu d'exception. On valide alors la transaction.
  • lignes 34-35 : il y a eu exception, on défait la transaction
  • ligne 37 : on sort de la clause using . La connexion ouverte en ligne 18 est automatiquement fermée.

Implémentation de la couche [dao]

L'implémentation Dao de l'interface IDao sera la suivante :

Cacher Sélectionnez
  1. using System.Collections.Generic;
  2. using System.Data;
  3. using System.Data.SqlServerCe;
  4. using System.Threading;
  5.
  6. namespace Chap8 {
  7.     public class Dao : IDao {
  8.         // chaîne de connexion
  9.         public string ConnectionString { get; set; }
  10.         // requêtes
  11.         public string InsertText { get; set; }
  12.         public string DeleteAllText { get; set; }
  13.         public string GetAllText { get; set; }
  14.
  15.         // implémentation interface
  16.
  17.         // insertion article
  18.         public int InsertArticle(Article article) {
  19.             // y-a-t-il une transaction en cours ?
  20.             SqlCeTransaction transaction = Thread.GetData(Thread.GetNamedDataSlot("transaction")) as SqlCeTransaction;
  21.             // récupérer la connexion ou la créer
  22.             SqlCeConnection connexion = null;
  23.             if (transaction != null) {
  24.                 // récupérer la connexion
  25.                 connexion = transaction.Connection as SqlCeConnection;
  26.             } else {
  27.                 // la créer
  28.                 connexion = new SqlCeConnection(ConnectionString);
  29.                 connexion.Open();
  30.             }
  31.             try {
  32.                 // préparation commande d'insertion
  33.                 SqlCeCommand sqlCommand = new SqlCeCommand();
  34.                 sqlCommand.Transaction = transaction;
  35.                 sqlCommand.Connection = connexion;
  36.                 sqlCommand.CommandText = InsertText;
  37.                 sqlCommand.Parameters.Add("@nom", SqlDbType.NVarChar, 30);
  38.                 sqlCommand.Parameters.Add("@prix", SqlDbType.Money);
  39.                 sqlCommand.Parameters.Add("@sa", SqlDbType.Int);
  40.                 sqlCommand.Parameters.Add("@sm", SqlDbType.Int);
  41.                 sqlCommand.Parameters["@nom"].Value = article.Nom;
  42.                 sqlCommand.Parameters["@prix"].Value = article.Prix;
  43.                 sqlCommand.Parameters["@sa"].Value = article.StockActuel;
  44.                 sqlCommand.Parameters["@sm"].Value = article.StockMinimum;
  45.                 // exécution
  46.                 return sqlCommand.ExecuteNonQuery();
  47.             } finally {
  48.                 // si on n'était pas dans une transaction, on ferme la connexion
  49.                 if (transaction == null) {
  50.                     connexion.Close();
  51.                 }
  52.             }
  53.         }
  54.
  55.         // liste des articles
  56.         public List<Article> GetAllArticles() {
  57. ...
  58.         }
  59.
  60.         // suppression des articles
  61.         public void DeleteAllArticles() {
  62. ...
  63.         }
  64.     }
  65. }

La classe a les propriétés suivantes :

  • ligne 9 : la chaîne de connexion qui permet de se connecter à la base de données des articles
  • ligne 11 : l'ordre SQL pour insérer un article
  • ligne 12 : l'ordre SQL pour suprrimer tous les articles
  • ligne 13 : l'ordre SQL pour obtenir tous les articles

Ces propriétés seront initialisées à partir du fichier de configuration [App.config] suivant :

Cacher Sélectionnez
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.     <connectionStrings>
  4.         <add name="dbArticlesSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
  5.     </connectionStrings>
  6.     <appSettings>
  7.         <add key="insertText" value="insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)"/>
  8.         <add key="getAllText" value="select id,nom,prix,stockactuel,stockminimum from articles"/>
  9.         <add key="deleteAllText" value="delete from articles"/>
  10.     </appSettings>
  11. </configuration>

Nous commentons la méthode InsertArticle :

  • ligne 20 : on récupère l'éventuelle transaction qu'a pu placer la couche [metier] dans le thread
  • lignes 23-25 : si la transaction est présente, on récupère la connexion à laquelle elle a été liée.
  • lignes 26-30 : sinon, une connexion nouvelle est créée et ouverte.
  • lignes 33-44 : on prépare la commande d'insertion. Celle-ci est paramétrée (cf ligne g de App.config ).
  • ligne 33 : l'objet Command est créé.
  • ligne 34 : il est associé à la transaction courante. Si celle-ci n'existe pas (transaction=null), cela revient à exécuter l'ordre SQL sans transaction explicite. On rappelle qu'alors il y a quand même une transaction implicite. Avec SQL Server CE, cette transaction implicite est par défaut en mode autocommit : l'ordre SQL est committé après son exécution.
  • ligne 35 : l'objet Command est associé à la connexion courante
  • ligne 36 : le texte SQl à exécuter est fixé. C'est la requête paramétrée de la ligne g de App.config .
  • lignes 37-44 : les 4 paramètres de la requête sont initialisés
  • ligne 46 : la requête est exécutée.
  • lignes 49-51 : il faut se souvenir que s'il n'y avait pas de transaction, une nouvelle connexion a été ouverte avec la base, lignes 26-30. Dans ce cas, elle doit être fermée. S'il y avait une transaction, la connexion ne doit pas être fermée car c'est la couche [metier] qui la gère.

Les deux autres méthodes reprennent ce qui a été vu dans le chapitre "Bases de données" :

Cacher Sélectionnez
  1.         // liste des articles
  2.         public List<Article> GetAllArticles() {
  3.             // liste des articles - vide au départ
  4.             List<Article> articles = new List<Article>();
  5.             // exploitation connexion
  6.             using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) {
  7.                 // ouverture connexion
  8.                 connexion.Open();
  9.                 // exécute sqlCommand avec requête select
  10.                 SqlCeCommand sqlCommand = new SqlCeCommand(GetAllText, connexion);
  11.                 using (SqlCeDataReader reader = sqlCommand.ExecuteReader()) {
  12.                     // exploitation résultat
  13.                     while (reader.Read()) {
  14.                         // exploitation ligne courante
  15.                         articles.Add(new Article(reader.GetInt32(0), reader.GetString(1), reader.GetDecimal(2), reader.GetInt32(3), reader.GetInt32(4)));
  16.                     }
  17.                 }
  18.             }
  19.             // on rend le résultat
  20.             return articles;
  21.         }
  22.
  23.         // suppression des articles
  24.         public void DeleteAllArticles() {
  25.             using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) {
  26.                 // ouverture connexion
  27.                 connexion.Open();
  28.                 // exécute sqlCommand avec requête de mise à jour
  29.                 new SqlCeCommand(DeleteAllText, connexion).ExecuteNonQuery();
  30.             }
  31. }

L'application [console] de test

L'application [console] de test est la suivante :

Cacher Sélectionnez
  1. using System;
  2. using System.Configuration;
  3.
  4. namespace Chap8 {
  5.     class Program {
  6.         static void Main(string[] args) {
  7.             // exploitation du fichier de configuration
  8.             string connectionString = null;
  9.             string insertText;
  10.             string getAllText;
  11.             string deleteAllText;
  12.             try {
  13.                 // chaîne de connexion
  14.                 connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString;
  15.                 // autres paramètres
  16.                 insertText = ConfigurationManager.AppSettings["insertText"];
  17.                 getAllText = ConfigurationManager.AppSettings["getAllText"];
  18.                 deleteAllText = ConfigurationManager.AppSettings["deleteAllText"];
  19.             } catch (Exception e) {
  20.                 Console.WriteLine("Erreur de configuration : {0}", e.Message);
  21.                 return;
  22.             }
  23.             // création couche [dao]
  24.             Dao dao = new Dao();
  25.             dao.ConnectionString = connectionString;
  26.             dao.DeleteAllText = deleteAllText;
  27.             dao.GetAllText = getAllText;
  28.             dao.InsertText = insertText;
  29.             // création couche [métier]
  30.             Metier metier = new Metier();
  31.             metier.Dao = dao;
  32.             metier.ConnectionString = connectionString;
  33.             // on crée un tableau d'articles
  34.             Article[] articles = new Article[2];
  35.             for (int i = 0; i < articles.Length; i++) {
  36.                 articles[i] = new Article(0, "article", 100, 10, 1);
  37.             }
  38.             // on supprime tous les articles
  39.             Console.WriteLine("Suppression de tous les articles...");
  40.             metier.DeleteAllArticles();
  41.             // on insère le tableau hors transaction
  42.             Console.WriteLine("Insertion des articles hors transaction...");
  43.             try {
  44.                 metier.InsertArticlesOutOfTransaction(articles);
  45.             } catch (Exception e){
  46.                 Console.WriteLine("Exception : {0}", e.Message);
  47.             }
  48.             // on affiche les articles
  49.             Console.WriteLine("Liste des articles");
  50.             AfficheArticles(metier);
  51.             // on supprime tous les articles
  52.             Console.WriteLine("Suppression de tous les articles...");
  53.             metier.DeleteAllArticles();
  54.             // on insère le tableau dans une transaction
  55.             Console.WriteLine("Insertion des articles dans une transaction...");
  56.             metier.InsertArticlesInTransaction(articles);
  57.             // on affiche les articles
  58.             Console.WriteLine("Liste des articles");
  59.             AfficheArticles(metier);
  60.         }
  61.
  62.         private static void AfficheArticles(IMetier metier) {
  63.             // on affiche les articles
  64.             foreach(Article article in metier.GetAllArticles()){
  65.                 Console.WriteLine(article);
  66.             }
  67.         }
  68.
  69.     }
  70. }
  • lignes 12-22 : le fichier [App.config] est exploité.
  • lignes 24-28 : la couche [dao] est instanciée et initialisée
  • lignes 30-32 : il est fait de même pour la couche [metier]
  • lignes 34-37 : on crée un tableau de 2 articles avec le même nom. La table [articles] de la base SQL server Ce [dbarticles.sdf] a une contrainte d'unicité sur le nom. L'insertion du 2ième article sera donc refusée. Si l'insertion du tableau se fait hors transaction, le 1er article sera d'abord inséré puis le restera. Si l'insertion du tableau se fait dans une transaction, le 1er article sera d'abord inséré puis sera retiré, lors du Rollback de la transaction.
  • lignes 39-50 : insertion hors transaction du tableau de 2 articles et vérification.
  • lignes 52-59 : idem mais dans une transaction

Les résultats à l'exécution sont les suivants :

Cacher Sélectionnez
  1. Suppression de tous les articles...
  2. Insertion des articles hors transaction...
  3. Exception : A duplicate value cannot be inserted into a unique index. [ Table na
  4. me = ARTICLES,Constraint name = UQ__ARTICLES__0000000000000010 ]
  5. Liste des articles
  6. [7,article,100,10,1]
  7. Suppression de tous les articles...
  8. Insertion des articles dans une transaction...
  9. Liste des articles
  • lignes 5-6 : l'insertion hors transaction a laissé le 1er article dans la base
  • ligne 9 : l'insertion faite dans une transaction n'a laissé aucun article dans la base

XI-I-3. Conclusion

L'exemple précédent a montré l'intérêt des données locales à un thread pour la gestion des transactions. Il n'est pas à reproduire tel quel. Des frameworks tels que Spring, Nhibernate… utilisent cette technique mais la rendent encore plus transparente : il est possible pour la couche [metier] d'utiliser des transactions sans que la couche [dao] n'ait besoin de le savoir. Il n'y a alors aucun objet Transaction dans le code de la couche [dao]. Cela est obtenu au moyen d'une technique de proxy appelée AOP (Aspects Oriented Programming). De nouveau on ne peut qu'inciter le lecteur à utiliser ces frameworks.

XI-J. Pour approfondir…

Pour approfondir le domaine difficile de la synchronisation de threads, on pourra lire le chapitre Threading du livre C# 3.0 référencé dans l'introduction de ce document. On y présente de nombreuses techniques de synchronisation pour différents types de situation.


CSharp tutoriel 2008

Apprentissage du langage C#

  • XII. Programmation Internet
    • XII-A. Généralités
      • XII-A-1. Les protocoles de l'Internet
      • XII-A-2. Le modèle OSI
      • XII-A-3. Le modèle TCP/IP
      • XII-A-4. Fonctionnement des protocoles de l'Internet
      • XII-A-5. Les problèmes d'adressage dans l'Internet
        • XII-A-5-a. Les classes d'adresses IP
        • XII-A-5-b. Les protocoles de conversion Adresse Internet <--> Adresse physique
      • XII-A-6. La couche réseau dite couche IP de l'internet
        • XII-A-6-a. Le routage
        • XII-A-6-b. Messages d'erreur et de contrôle
      • XII-A-7. La couche transport : les protocoles UDP et TCP
        • XII-A-7-a. Le protocole UDP : User Datagram Protocol
        • XII-A-7-b. Le protocole TCP : Transfer Control Protocol
      • XII-A-8. La couche Applications
      • XII-A-9. Conclusion
    • XII-B. Les classes .NET de la gestion des adresses IP
    • XII-C. Les bases de la programmation internet
      • XII-C-1. Généralités
      • XII-C-2. Les caractéristiques du protocole TCP
      • XII-C-3. La relation client-serveur
      • XII-C-4. Architecture d'un client
      • XII-C-5. Architecture d'un serveur
    • XII-D. Découvrir les protocoles de communication de l'internet
      • XII-D-1. Introduction
      • XII-D-2. Le protocole HTTP (HyperText Transfer Protocol)
      • XII-D-3. Le protocole SMTP (Simple Mail Transfer Protocol)
      • XII-D-4. Le protocole POP (Post Office Protocol)
      • XII-D-5. Le protocole FTP (File Transfer Protocol)
    • XII-E. Les classes .NET de la programmation internet
      • XII-E-1. Choisir la classe adaptée
      • XII-E-2. La classe TcpClient
      • XII-E-3. La classe TcpListener
    • XII-F. Exemples de clients / serveurs TCP
      • XII-F-1. Un serveur d'écho
      • XII-F-2. Un client pour le serveur d'écho
      • XII-F-3. Un client TCP générique
      • XII-F-4. Un serveur Tcp générique
      • XII-F-5. Un client Web
      • XII-F-6. Un client Web gérant les redirections
    • XII-G. Les classes .NET spécialisées dans un protocole particulier de l'internet
      • XII-G-1. La classeWebClient
      • XII-G-2. Les classes WebRequest / WebResponse
      • XII-G-3. Application : un client proxy d'un serveur web de traduction
        • XII-G-3-a. L'application
        • XII-G-3-b. L'architecture de l'application
        • XII-G-3-c. Le projet Visual studio
        • XII-G-3-d. Le projet [dao]
        • XII-G-3-e. Le client web [ServiceTraduction]
        • XII-G-3-f. L'interface graphique de l'application
        • XII-G-3-g. Conclusion
      • XII-G-4. Un client SMTP (Simple Mail Tranport Protocol) avec la classe SmtpClient
    • XII-H. Un client Tcp générique asynchrone
      • XII-H-1. Présentation
      • XII-H-2. L'interface graphique du client Tcp asynchrone
      • XII-H-3. Connexion asynchrone au serveur
      • XII-H-4. Déconnexion du serveur
      • XII-H-5. Envoi asynchrone de données au serveur
      • XII-H-6. Affichage des événements et du dialogue client / serveur
      • XII-H-7. Conclusion
    • XII-I. Application exemple, version 8 : Serveur de calcul d'impôts
      • XII-I-1. L'architecture de la nouvelle version
      • XII-I-2. Le serveur de calcul d'impôt
        • XII-I-2-a. Le projet Visual Studio
        • XII-I-2-b. Configuration de l'application
        • XII-I-2-c. Fonctionnement du serveur
      • XII-I-3. Le code du serveur Tcp de calcul d'impôt
      • XII-I-4. Le client graphique du serveur Tcp de calcul d'impôt
        • XII-I-4-a. Le projet Visual Studio
        • XII-I-4-b. La couche [metier]
        • XII-I-4-c. La couche [ui]
      • XII-I-5. Conclusion

XII. Programmation Internet

XII-A. Généralités

XII-A-1. Les protocoles de l'Internet

Nous donnons ici une introduction aux protocoles de communication de l'Internet, appelés aussi suite de protocoles TCP/IP ( Transfer Control Protocol / Internet Protocol ), du nom des deux principaux protocoles. Il peut être utile que le lecteur ait une compréhension globale du fonctionnement des réseaux et notamment des protocoles TCP/IP avant d'aborder la construction d'applications distribuées. Le texte qui suit est une traduction partielle d'un texte que l'on trouve dans le document "Lan Workplace for Dos - Administrator's Guide" de NOVELL, document du début des années 90.

-----------------------------------

Le concept général de créer un réseau d'ordinateurs hétérogènes vient de recherches effectuées par le DARPA (Defense Advanced Research Projects Agency) aux Etats-Unis. Le DARPA a développé la suite de protocoles connue sous le nom de TCP/IP qui permet à des machines hétérogènes de communiquer entre elles. Ces protocoles ont été testés sur un réseau appelé ARPAnet, réseau qui devint ultérieurement le réseau INTERNET. Les protocoles TCP/IP définissent des formats et des règles de transmission et de réception indépendants de l'organisation des réseaux et des matériels utilisés.

Le réseau conçu par le DARPA et géré par les protocoles TCP/IP est un réseau à commutation de paquets. Un tel réseau transmet l'information sur le réseau, en petits morceaux appelés paquets. Ainsi, si un ordinateur transmet un gros fichier, ce dernier sera découpé en petits morceaux qui seront envoyés sur le réseau pour être recomposés à destination. TCP/IP définit le format de ces paquets, à savoir :

  • origine du paquet
  • destination
  • longueur
  • type

XII-A-2. Le modèle OSI

Les protocoles TCP/IP suivent à peu près le modèle de réseau ouvert appelé OSI (Open Systems Interconnection Reference Model) défini par l'ISO (International Standards Organisation). Ce modèle décrit un réseau idéal où la communication entre machines peut être représentée par un modèle à sept couches :

Image non disponible

Chaque couche reçoit des services de la couche inférieure et offre les siens à la couche supérieure. Supposons que deux applications situées sur des machines A et B différentes veulent communiquer : elles le font au niveau de la couche Application . Elles n'ont pas besoin de connaître tous les détails du fonctionnement du réseau : chaque application remet l'information qu'elle souhaite transmettre à la couche du dessous : la couche Présentation . L'application n'a donc à connaître que les règles d'interfaçage avec la couche Présentation .

Une fois l'information dans la couche Présentation , elle est passée selon d'autres règles à la couche Session et ainsi de suite, jusqu'à ce que l'information arrive sur le support physique et soit transmise physiquement à la machine destination. Là, elle subira le traitement inverse de celui qu'elle a subi sur la machine expéditeur.

A chaque couche, le processus expéditeur chargé d'envoyer l'information, l'envoie à un processus récepteur sur l'autre machine apartenant à la même couche que lui. Il le fait selon certaines règles que l'on appelle le protocole de la couche. On a donc le schéma de communication final suivant :

Image non disponible

Le rôle des différentes couches est le suivant :

Physique Assure la transmission de bits sur un support physique. On trouve dans cette couche des équipements terminaux de traitement des données (E.T.T.D.) tels que terminal ou ordinateur, ainsi que des équipements de terminaison de circuits de données (E.T.C.D.) tels que modulateur/démodulateur, multiplexeur, concentrateur. Les points d'intérêt à ce niveau sont :
  • le choix du codage de l'information (analogique ou numérique)
  • le choix du mode de transmission (synchrone ou asynchrone).
Liaison de données | Masque les particularités physiques de la couche Physique. Détecte et corrige les erreurs de transmission.
Réseau | Gère le chemin que doivent suivre les informations envoyées sur le réseau. On appelle cela le routage : déterminer la route à suivre par une information pour qu'elle arrive à son destinataire.
Transport | Permet la communication entre deux applications alors que les couches précédentes ne permettaient que la communication entre machines. Un service fourni par cette couche peut être le multiplexage : la couche transport pourra utiliser une même connexion réseau (de machine à machine) pour transmettre des informations appartenant à plusieurs applications.
Session | On va trouver dans cette couche des services permettant à une application d'ouvrir et de maintenir une session de travail sur une machine distante.
Présentation | Elle vise à uniformiser la représentation des données sur les différentes machines. Ainsi des données provenant d'une machine A, vont être "habillées" par la couche Présentation de la machine A, selon un format standard avant d'être envoyées sur le réseau. Parvenues à la couche Présentation de la machine destinatrice B qui les reconnaîtra grâce à leur format standard, elles seront habillées d'une autre façon afin que l'application de la machine B les reconnaisse.
Application | A ce niveau, on trouve les applications généralement proches de l'utilisateur telles que la messagerie électronique ou le transfert de fichiers.

XII-A-3. Le modèle TCP/IP

Le modèle OSI est un modèle idéal encore jamais réalisé. La suite de protocoles TCP/IP s'en approche sous la forme suivante :

Image non disponible

Couche Physique

En réseau local, on trouve généralement une technologie Ethernet ou Token-Ring. Nous ne présentons ici que la technologie Ethernet.

Ethernet

C'est le nom donné à une technologie de réseaux locaux à commutation de paquets inventée à PARC Xerox au début des années 1970 et normalisée par Xerox, Intel et Digital Equipment en 1978. Le réseau est physiquement constitué d'un câble coaxial d'environ 1,27 cm de diamètre et d'une longueur de 500 m au plus. Il peut être étendu au moyen de répéteurs , deux machines ne pouvant être séparées par plus de deux répéteurs. Le câble est passif : tous les éléments actifs sont sur les machines raccordées au câble. Chaque machine est reliée au câble par une carte d'accès au réseau comprenant :

  • un transmetteur ( transceiver ) qui détecte la présence de signaux sur le câble et convertit les signaux analogiques en signaux numérique et inversement.
  • un coupleur qui reçoit les signaux numériques du transmetteur et les transmet à l'ordinateur pour traitement ou inversement.

Les caractéristiques principales de la technologie Ethernet sont les suivantes :

  • Capacité de 10 Mégabits/seconde.
  • Topologie en bus : toutes les machines sont raccordées au même câble
Image non disponible
  • Réseau diffusant - Une machine qui émet transfère des informations sur le câble avec l'adresse de la machine destinatrice. Toutes les machines raccordées reçoivent alors ces informations et seule celle à qui elles sont destinées les conserve.
  • La méthode d'accès est la suivante : le transmetteur désirant émettre écoute le câble - il détecte alors la présence ou non d'une onde porteuse, présence qui signifierait qu'une transmission est en cours. C'est la technique CSMA ( Carrier Sense Multiple Access ). En l'absence de porteuse, un transmetteur peut décider de transmettre à son tour. Ils peuvent être plusieurs à prendre cette décision. Les signaux émis se mélangent : on dit qu'il y a collision. Le transmetteur détecte cette situation : en même temps qu'il émet sur le câble, il écoute ce qui passe réellement sur celui-ci. S'il détecte que l'information transitant sur le câble n'est pas celle qu'il a émise, il en déduit qu'il y a collision et il s'arrêtera d'émettre. Les autres transmetteurs qui émettaient feront de même. Chacun reprendra son émission après un temps aléatoire dépendant de chaque transmetteur. Cette technique est appelée CD ( Collision Detect ). La méthode d'accès est ainsi appelée CSMA/CD.
  • un adressage sur 48 bits. Chaque machine a une adresse, appelée ici adresse physique, qui est inscrite sur la carte qui la relie au câble. On appelle cet adresse, l'adresse Ethernet de la machine.

Couche Réseau

Nous trouvons au niveau de cette couche, les protocoles IP, ICMP, ARP et RARP.

IP (Internet Protocol) | Délivre des paquets entre deux nœuds du réseau
ICMP (Internet Control Message Protocol) | ICMP réalise la communication entre le programme du protocole IP d'une machine et celui d'une autre machine. C'est donc un protocole d'échange de messages à l'intérieur même du protocole IP.
ARP (Address Resolution Protocol) | fait la correspondance adresse Internet machine--> adresse physique machine
RARP (Reverse Address Resolution Protocol) | fait la correspondance adresse physique machine--> adresse Internet machine

Couches Transport/Session

Dans cette couche, on trouve les protocoles suivants :

TCP (Transmission Control Protocol) | Assure une remise fiable d'informations entre deux clients
UDP (User Datagram Protocol) | Assure une remise non fiable d'informations entre deux clients

Couches Application/Présentation/Session

On trouve ici divers protocoles :

TELNET | Emulateur de terminal permettant à une machine A de se connecter à une machine B en tant que terminal
FTP (File Transfer Protocol) | permet des transferts de fichiers
TFTP (Trivial File Transfer Protocol) | permet des transferts de fichiers
SMTP (Simple Mail Transfer protocol) | permet l'échange de messages entre utilisateurs du réseau
DNS (Domain Name System) | transforme un nom de machine en adresse Internet de la machine
XDR (eXternal Data Representation) | créé par sun MicroSystems, il spécifie une représentation standard des données, indépendante des machines
RPC(Remote Procedures Call) | défini également par Sun, c'est un protocole de communication entre applications distantes, indépendant de la couche transport. Ce protocole est important : il décharge le programmeur de la connaissance des détails de la couche transport et rend les applications portables. Ce protocole s'appuie sur sur le protocole XDR
NFS (Network File System) | toujours défini par Sun, ce protocole permet à une machine, de "voir" le système de fichiers d'une autre machine. Il s'appuie sur le protocole RPC précédent

XII-A-4. Fonctionnement des protocoles de l'Internet

Les applications développées dans l'environnement TCP/IP utilisent généralement plusieurs des protocoles de cet environnement. Un programme d'application communique avec la couche la plus élevée des protocoles. Celle-ci passe l'information à la couche du dessous et ainsi de suite jusqu'à arriver sur le support physique. Là, l'information est physiquement transférée à la machine destinatrice où elle retraversera les mêmes couches, en sens inverse cette fois-ci, jusqu'à arriver à l'application destinatrice des informations envoyées. Le schéma suivant montre le parcours de l'information :

Image non disponible

Prenons un exemple : l'application FTP, définie au niveau de la couche Application et qui permet des transferts de fichiers entre machines.

  • L'application délivre une suite d'octets à transmettre à la couche transport.
  • La couche transport découpe cette suite d'octets en segments TCP, et ajoute au début de chaque segment, le numéro de celui-ci. Les segments sont passés à la couche Réseau gouvernée par le protocole IP.
  • La couche IP crée un paquet encapsulant le segment TCP reçu. En tête de ce paquet, elle place les adresses Internet des machines source et destination. Elle détermine également l'adresse physique de la machine destinatrice. Le tout est passé à la couche Liaison de données & Liaison physique , c'est-à-dire à la carte réseau qui couple la machine au réseau physique.
  • Là, le paquet IP est encapsulé à son tour dans une trame physique et envoyé à son destinataire sur le câble.
  • Sur la machine destinatrice, la couche Liaison de données & Liaison physique fait l'inverse : elle désencapsule le paquet IP de la trame physique et le passe à la couche IP.
  • La couche IP vérifie que le paquet est correct : elle calcule une somme, fonction des bits reçus ( checksum ), somme qu'elle doit retrouver dans l'en-tête du paquet. Si ce n'est pas le cas, celui-ci est rejeté.
  • Si le paquet est déclaré correct, la couche IP désencapsule le segment TCP qui s'y trouve et le passe au-dessus à la couche transport .
  • La couche transport , couche TCP dans notre exemple, examine le numéro du segment afin de restituer le bon ordre des segments.
  • Elle calcule également une somme de vérification pour le segment TCP. S'il est trouvé correct, la couche TCP envoie un accusé de réception à la machine source, sinon le segment TCP est refusé.
  • Il ne reste plus à la couche TCP qu'à transmettre la partie données du segment à l'application destinatrice de celles-ci dans la couche du dessus.

XII-A-5. Les problèmes d'adressage dans l'Internet

Un nœud d'un réseau peut être un ordinateur, une imprimante intelligente, un serveur de fichiers, n'importe quoi en fait pouvant communiquer à l'aide des protocoles TCP/IP. Chaque nœud a une adresse physique ayant un format dépendant du type du réseau. Sur un réseau Ethernet, l'adresse physique est codée sur 6 octets. Une adresse d'un réseau X25 est un nombre à 14 chiffres.

L'adresse Internet d'un nœud est une adresse logique : elle est indépendante du matériel et du réseau utilisé. C'est une adresse sur 4 octets identifiant à la fois un réseau local et un nœud de ce réseau. L'adresse Internet est habituellement représentée sous la forme de 4 nombres, valeurs des 4 octets, séparés par un point. Ainsi l'adresse de la machine Lagaffe de la faculté des Sciences d'Angers est notée 193.49.144.1 et celle de la machine Liny 193.49.144.9. On en déduira que l'adresse Internet du réseau local est 193.49.144.0. On pourra avoir jusqu'à 254 nœuds sur ce réseau.

Parce que les adresses Internet ou adresses IP sont indépendantes du réseau, une machine d'un réseau A peut communiquer avec une machine d'un réseau B sans se préoccuper du type de réseau sur lequel elle se trouve : il suffit qu'elle connaisse son adresse IP. Le protocole IP de chaque réseau se charge de faire la conversion adresse IP <--> adresse physique, dans les deux sens.

Les adresses IP doivent être toutes différentes. En France, c'est l'INRIA qui s'occupe d'affecter les adresses IP. En fait, cet organisme délivre une adresse pour votre réseau local, par exemple 193.49.144.0 pour le réseau de la faculté des sciences d'Angers. L'administrateur de ce réseau peut ensuite affecter les adresses IP 193.49.144.1 à 193.49.144.254 comme il l'entend. Cette adresse est généralement inscrite dans un fichier particulier de chaque machine reliée au réseau.

XII-A-5-a. Les classes d'adresses IP

Une adresse IP est une suite de 4 octets notée souvent I1.I2.I3.I4, qui contient en fait deux adresses :

  • l'adresse du réseau
  • l'adresse d'un nœud de ce réseau

Selon la taille de ces deux champs, les adresses IP sont divisées en 3 classes : classes A, B et C.

Classe A

L'adresse IP : I1.I2.I3.I4 a la forme R1.N1.N2.N3 où

R1est l'adresse du réseau

N1.N2.N3est l'adresse d'une machine dans ce réseau

Plus exactement, la forme d'une adresse IP de classe A est la suivante :

L'adresse réseau est sur 7 bits et l'adresse du nœud sur 24 bits. On peut donc avoir 127 réseaux de classe A, chacun comportant jusqu'à 224 nœuds.

Classe B

Ici, l'adresse IP : I1.I2.I3.I4 a la forme R1.R2.N1.N2 où

R1.R2est l'adresse du réseau

N1.N2est l'adresse d'une machine dans ce réseau

Plus exactement, la forme d'une adresse IP de classe B est la suivante :

L'adresse du réseau est sur 2 octets (14 bits exactement) ainsi que celle du nœud. On peut donc avoir 214 réseaux de classe B chacun comportant jusqu'à 216 nœuds.

Classe C

Dans cette classe, l'adresse IP : I1.I2.I3.I4 a la forme R1.R2.R3.N1 où

R1.R2.R3est l'adresse du réseau

N1est l'adresse d'une machine dans ce réseau

Plus exactement, la forme d'une adresse IP de classe C est la suivante :

L'adresse réseau est sur 3 octets (moins 3 bits) et l'adresse du nœud sur 1 octet. On peut donc avoir 221 réseaux de classe C comportant jusqu'à 256 nœuds.

L'adresse de la machine Lagaffe de la faculté des sciences d'Angers étant 193.49.144.1, on voit que l'octet de poids fort vaut 193, c'est-à-dire en binaire 11000001. On en déduit que le réseau est de classe C.

Adresses réservées

  • Certaines adresses IP sont des adresses de réseaux plutôt que des adresses de nœuds dans le réseau. Ce sont celles, où l'adresse du nœud est mise à 0. Ainsi, l'adresse 193.49.144.0 est l'adresse IP du réseau de la Faculté des Sciences d'Angers. En conséquence, aucun nœud d'un réseau ne peut avoir l'adresse zéro.
  • Lorsque dans une adresse IP, l'adresse du nœud ne comporte que des 1, on a alors une adresse de diffusion : cette adresse désigne tous les nœuds du réseau.
  • Dans un réseau de classe C, permettant théoriquement 28=256 nœuds, si on enlève les deux adresses interdites, on n'a plus que 254 adresses autorisées.

XII-A-5-b. Les protocoles de conversion Adresse Internet <--> Adresse physique

Nous avons vu que lors d'une émission d'informations d'une machine vers une autre, celles-ci à la traversée de la couche IP étaient encapsulées dans des paquets. Ceux-ci ont la forme suivante :

Le paquet IP contient donc les adresses Internet des machines source et destination. Lorsque ce paquet va être transmis à la couche chargée de l'envoyer sur le réseau physique, d'autres informations lui sont ajoutées pour former la trame physique qui sera finalement envoyée sur le réseau. Par exemple, le format d'une trame sur un réseau Ethernet est le suivant :

Dans la trame finale, il y a l'adresse physique des machines source et destination. Comment sont-elles obtenues ?

La machine expéditrice connaissant l'adresse IP de la machine avec qui elle veut communiquer obtient l'adresse physique de celle-ci en utilisant un protocole particulier appelé ARP ( Address Resolution Protocol ).

  • Elle envoie un paquet d'un type spécial appelé paquet ARP contenant l'adresse IP de la machine dont on cherche l'adresse physique. Elle a pris soin également d'y placer sa propre adresse IP ainsi que son adresse physique.
  • Ce paquet est envoyé à tous les nœuds du réseau.
  • Ceux-ci reconnaissent la nature spéciale du paquet. Le nœud qui reconnaît son adresse IP dans le paquet, répond en envoyant à l'expéditeur du paquet son adresse physique. Comment le peut-il ? Il a trouvé dans le paquet les adresses IP et physique de l'expéditeur.
  • L'expéditeur reçoit donc l'adresse physique qu'il cherchait. Il la stocke en mémoire afin de pouvoir l'utiliser ultérieurement si d'autres paquets sont à envoyer au même destinataire.

L'adresse IP d'une machine est normalement inscrite dans l'un de ses fichiers qu'elle peut donc consulter pour la connaître. Cette adresse peut être changée : il suffit d'éditer le fichier. L'adresse physique elle, est inscrite dans une mémoire de la carte réseau et ne peut être changée.

Lorsqu'un administrateur désire d'organiser son réseau différemment, il peut être amené à changer les adresses IP de tous les nœuds et donc à éditer les différents fichiers de configuration des différents nœuds. Cela peut être fastidieux et une occasion d'erreurs s'il y a beaucoup de machines. Une méthode consiste à ne pas affecter d'adresse IP aux machines : on inscrit alors un code spécial dans le fichier dans lequel la machine devrait trouver son adresse IP. Découvrant qu'elle n'a pas d'adresse IP, la machine la demande selon un protocole appelé RARP (Reverse Address Resolution Protocol). Elle envoie alors sur un réseau un paquet spécial appelé paquet RARP, analogue au paquet ARP précédent, dans lequel elle met son adresse physique. Ce paquet est envoyé à tous les nœuds qui reconnaissent alors un paquet RARP. L'un d'entre-eux, appelé serveur RARP, possède un fichier donnant la correspondance adresse physique <--> adresse IP de tous les nœuds. Il répond alors à l'expéditeur du paquet RARP, en lui renvoyant son adresse IP. Un administrateur désirant reconfigurer son réseau, n'a donc qu'à éditer le fichier de correspondances du serveur RARP. Celui-ci doit normalement avoir une adresse IP fixe qu'il doit pouvoir connaître sans avoir à utiliser lui-même le protocole RARP.

XII-A-6. La couche réseau dite couche IP de l'internet

Le protocole IP (Internet Protocol) définit la forme que les paquets doivent prendre et la façon dont ils doivent être gérés lors de leur émission ou de leur réception. Ce type de paquet particulier est appelé un datagramme IP. Nous l'avons déjà présenté :

L'important est qu'outre les données à transmettre, le datagramme IP contient les adresses Internet des machines source et destination. Ainsi la machine destinatrice sait qui lui envoie un message.

A la différence d'une trame de réseau qui a une longueur déterminée par les caractéristiques physiques du réseau sur lequel elle transite, la longueur du datagramme IP est elle fixée par le logiciel et sera donc la même sur différents réseaux physiques. Nous avons vu qu'en descendant de la couche réseau dans la couche physique le datagramme IP était encapsulé dans une trame physique. Nous avons donné l'exemple de la trame physique d'un réseau Ethernet :

Les trames physiques circulent de nœud en nœud vers leur destination qui peut ne pas être sur le même réseau physique que la machine expéditrice. Le paquet IP peut donc être encapsulé successivement dans des trames physiques différentes au niveau des nœuds qui font la jonction entre deux réseaux de type différent. Il se peut aussi que le paquet IP soit trop grand pour être encapsulé dans une trame physique. Le logiciel IP du nœud où se pose ce problème, décompose alors le paquet IP en fragments selon des règles précises, chacun d'eux étant ensuite envoyé sur le réseau physique. Ils ne seront réassemblés qu'à leur ultime destination.

XII-A-6-a. Le routage

Le routage est la méthode d'acheminement des paquets IP à leur destination. Il y a deux méthodes : le routage direct et le routage indirect.

Routage direct

Le routage direct désigne l'acheminement d'un paquet IP directement de l'expéditeur au destinataire à l'intérieur du même réseau :

  • La machine expéditrice d'un datagramme IP a l'adresse IP du destinataire.
  • Elle obtient l'adresse physique de ce dernier par le protocole ARP ou dans ses tables, si cette adresse a déjà été obtenue.
  • Elle envoie le paquet sur le réseau à cette adresse physique.

Routage indirect

Le routage indirect désigne l'acheminement d'un paquet IP à une destination se trouvant sur un autre réseau que celui auquel appartient l'expéditeur. Dans ce cas, les parties adresse réseau des adresses IP des machines source et destination sont différentes. La machine source reconnaît ce point. Elle envoie alors le paquet à un nœud spécial appelé routeur ( router ), nœud qui connecte un réseau local aux autres réseaux et dont elle trouve l'adresse IP dans ses tables, adresse obtenue initialement soit dans un fichier soit dans une mémoire permanente ou encore via des informations circulant sur le réseau.

Un routeur est attaché à deux réseaux et possède une adresse IP à l'intérieur de ces deux réseaux.

Dans notre exemple ci-dessus :

.Le réseau n° 1 a l'adresse Internet 193.49.144.0 et le réseau n° 2 l'adresse 193.49.145.0.

.A l'intérieur du réseau n° 1, le routeur a l'adresse 193.49.144.6 et l'adresse 193.49.145.3 à l'intérieur du réseau n° 2.

Le routeur a pour rôle de mettre le paquet IP qu'il reçoit et qui est contenu dans une trame physique typique du réseau n° 1, dans une trame physique pouvant circuler sur le réseau n° 2. Si l'adresse IP du destinataire du paquet est dans le réseau n° 2, le routeur lui enverra le paquet directement sinon il l'enverra à un autre routeur, connectant le réseau n° 2 à un réseau n° 3 et ainsi de suite.

XII-A-6-b. Messages d'erreur et de contrôle

Toujours dans la couche réseau, au même niveau donc que le protocole IP, existe le protocole ICMP ( Internet Control Message Protocol ). Il sert à envoyer des messages sur le fonctionnement interne du réseau : nœuds en panne, embouteillage à un routeur, etc … Les messages ICMP sont encapsulés dans des paquets IP et envoyés sur le réseau. Les couches IP des différents nœuds prennent les actions appropriées selon les messages ICMP qu'elles reçoivent. Ainsi, une application elle-même, ne voit jamais ces problèmes propres au réseau.

Un nœud utilisera les informations ICMP pour mettre à jour ses tables de routage.

XII-A-7. La couche transport : les protocoles UDP et TCP

XII-A-7-a. Le protocole UDP : User Datagram Protocol

Le protocole UDP permet un échange non fiable de données entre deux points, c'est-à-dire que le bon acheminement d'un paquet à sa destination n'est pas garanti. L'application, si elle le souhaite peut gérer cela elle-même, en attendant par exemple après l'envoi d'un message, un accusé de réception, avant d'envoyer le suivant.

Pour l'instant, au niveau réseau, nous avons parlé d'adresses IP de machines. Or sur une machine, peuvent coexister en même temps différents processus qui tous peuvent communiquer. Il faut donc indiquer, lors de l'envoi d'un message, non seulement l'adresse IP de la machine destinatrice, mais également le "nom" du processus destinataire. Ce nom est en fait un numéro, appelé numéro de port. Certains numéros sont réservés à des applications standard : port 69 pour l'application tftp (trivial file transfer protocol) par exemple.

Les paquets gérés par le protocole UDP sont appelés également des datagrammes. Ils ont la forme suivante :

Ces datagrammes seront encapsulés dans des paquets IP, puis dans des trames physiques.

XII-A-7-b. Le protocole TCP : Transfer Control Protocol

Pour des communications sûres, le protocole UDP est insuffisant : le développeur d'applications doit élaborer lui-même un protocole lui permettant de détecter le bon acheminement des paquets. Le protocole TCP (Transfer Control Protocol) évite ces problèmes. Ses caractéristiques sont les suivantes :

  • Le processus qui souhaite émettre établit tout d'abord une connexion avec le processus destinataire des informations qu'il va émettre. Cette connexion se fait entre un port de la machine émettrice et un port de la machine réceptrice. Il y a entre les deux ports un chemin virtuel qui est ainsi créé et qui sera réservé aux deux seuls processus ayant réalisé la connexion.
  • Tous les paquets émis par le processus source suivent ce chemin virtuel et arrivent dans l'ordre où ils ont été émis ce qui n'était pas garanti dans le protocole UDP puisque les paquets pouvaient suivre des chemins différents.
  • L'information émise a un aspect continu. Le processus émetteur envoie des informations à son rhythme. Celles-ci ne sont pas nécessairement envoyées tout de suite : le protocole TCP attend d'en avoir assez pour les envoyer. Elles sont stockées dans une structure appelée segment TCP . Ce segment une fois rempli sera transmis à la couche IP où il sera encapsulé dans un paquet IP.
  • Chaque segment envoyé par le protocole TCP est numéroté. Le protocole TCP destinataire vérifie qu'il reçoit bien les segments en séquence. Pour chaque segment correctement reçu, il envoie un accusé de réception à l'expéditeur.
  • Lorsque ce dernier le reçoit, il l'indique au processus émetteur. Celui-ci peut donc savoir qu'un segment est arrivé à bon port, ce qui n'était pas possible avec le protocole UDP.
  • Si au bout d'un certain temps, le protocole TCP ayant émis un segment ne reçoit pas d'accusé de réception, il retransmet le segment en question, garantissant ainsi la qualité du service d'acheminement de l'information.
  • Le circuit virtuel établi entre les deux processus qui communiquent est full-duplex : cela signifie que l'information peut transiter dans les deux sens. Ainsi le processus destination peut envoyer des accusés de réception alors même que le processus source continue d'envoyer des informations. Cela permet par exemple au protocole TCP source d'envoyer plusieurs segments sans attendre d'accusé de réception. S'il réalise au bout d'un certain temps qu'il n'a pas reçu l'accusé de réception d'un certain segment n° n , il reprendra l'émission des segments à ce point.

XII-A-8. La couche Applications

Au-dessus des protocoles UDP et TCP, existent divers protocoles standard :

TELNET

Ce protocole permet à un utilisateur d'une machine A du réseau de se connecter sur une machine B (appelée souvent machine hôte). TELNET émule sur la machine A un terminal dit universel. L'utilisateur se comporte donc comme s'il disposait d'un terminal connecté à la machine B. Telnet s'appuie sur le protocole TCP.

FTP : (File Transfer protocol)

Ce protocole permet l'échange de fichiers entre deux machines distantes ainsi que des manipulations de fichiers tels que des créations de répertoire par exemple. Il s'appuie sur le protocole TCP.

TFTP: (Trivial File Transfer Control)

Ce protocole est une variante de FTP. Il s'appuie sur le protocole UDP et est moins sophistiqué que FTP.

DNS : (Domain Name System)

Lorsqu'un utilisateur désire échanger des fichiers avec une machine distante, par FTP par exemple, il doit connaître l'adresse Internet de cette machine. Par exemple, pour faire du FTP sur la machine Lagaffe de l'université d'Angers, il faudrait lancer FTP comme suit : FTP 193.49.144.1

Cela oblige à avoir un annuaire faisant la correspondance machine <--> adresse IP. Probablement que dans cet annuaire les machines seraient désignées par des noms symboliques tels que :

machine DPX2/320 de l'université d'Angers

machine Sun de l'ISERPA d'Angers

On voit bien qu'il serait plus agréable de désigner une machine par un nom plutôt que par son adresse IP. Se pose alors le problème de l'unicité du nom : il y a des millions de machines interconnectées. On pourrait imaginer qu'un organisme centralisé attribue les noms. Ce serait sans doute assez lourd. Le contrôle des noms a été en fait distribué dans des domaines. Chaque domaine est géré par un organisme généralement très léger qui a toute liberté quant au choix des noms de machines. Ainsi les machines en France appartiennent au domaine fr, domaine géré par l'Inria de Paris. Pour continuer à simplifier les choses, on distribue encore le contrôle : des domaines sont créés à l'intérieur du domaine fr. Ainsi l'université d'Angers appartient au domaine univ-Angers. Le service gérant ce domaine a toute liberté pour nommer les machines du réseau de l'Université d'Angers. Pour l'instant ce domaine n'a pas été subdivisé. Mais dans une grande université comportant beaucoup de machines en réseau, il pourrait l'être.

La machine DPX2/320 de l'université d'Angers a été nommée Lagaffe alors qu'un PC 486DX50 a été nommé liny . Comment référencer ces machines de l'extérieur ? En précisant la hiérarchie des domaines auxquelles elles appartiennent. Ainsi le nom complet de la machine Lagaffe sera :

Lagaffe.univ-Angers.fr

A l'intérieur des domaines, on peut utiliser des noms relatifs. Ainsi à l'intérieur du domaine fr et en dehors du domaine univ-Angers, la machine Lagaffe pourra être référencée par

Lagaffe.univ-Angers

Enfin, à l'intérieur du domaine univ-Angers , elle pourra être référencée simplement par

Lagaffe

Une application peut donc référencer une machine par son nom. Au bout du compte, il faut quand même obtenir l'adresse Internet de cette machine. Comment cela est-il réalisé ? Suposons que d'une machine A, on veuille communiquer avec une machine B.

  • si la machine B appartient au même domaine que la machine A, on trouvera probablement son adresse IP dans un fichier de la machine A.
  • sinon, la machine A trouvera dans un autre fichier ou le même que précédemment, une liste de quelques serveurs de noms avec leurs adresses IP. Un serveur de noms est chargé de faire la correspondance entre un nom de machine et son adresse IP. La machine A va envoyer une requête spéciale au premier serveur de nom de sa liste, appelé requête DNS incluant donc le nom de la machine recherchée. Si le serveur interrogé a ce nom dans ses tablettes, il enverra à la machine A, l'adresse IP correspondante. Sinon, le serveur trouvera lui aussi dans ses fichiers, une liste de serveurs de noms qu'il peut interroger. Il le fera alors. Ainsi un certain nombre de serveurs de noms vont être interrogés, pas de façon anarchique mais d'une façon à minimiser les requêtes. Si la machine est finalement trouvée, la réponse redescendra jusqu'à la machine A.

XDR : (eXternal Data Representation)

Créé par sun MicroSystems, ce protocole spécifie une représentation standard des données, indépendante des machines.

RPC : (Remote Procedure Call)

Défini également par sun, c'est un protocole de communication entre applications distantes, indépendant de la couche transport. Ce protocole est important : il décharge le programmeur de la connaissance des détails de la couche transport et rend les applications portables. Ce protocole s'appuie sur sur le protocole XDR

NFS : Network File System

Toujours défini par Sun, ce protocole permet à une machine, de "voir" le système de fichiers d'une autre machine. Il s'appuie sur le protocole RPC précédent.

XII-A-9. Conclusion

Nous avons présenté dans cette introduction quelques grandes lignes des protocoles Internet. Pour approfondir ce domaine, on pourra lire l'excellent livre de Douglas Comer :

TitreTCP/IP : Architecture, Protocoles, Applications.

AuteurDouglas COMER

EditeurInterEditions

XII-B. Les classes .NET de la gestion des adresses IP

Une machine sur le réseau Internet est définie de façon unique par une adresse IP (Internet Protocol) qui peut prendre deux formes :

  • IPv4 : codée sur 32 bits et représentée par une chaîne de la forme "I1.I2.I3.I4" où In est un nombre entre 1 et 254. Ce sont les adresses IP les plus courantes actuellement.
  • IPv6 : codée sur 128 bits et représentée par une chaîne de la forme "[I1.I2.I3.I4.I5.I6.I7.I8]" où In est une chaîne de 4 chiffres hexadécimaux. Dans ce document, nous n'utiliserons pas les adresses IPv6.

Une machine peut être aussi définie par un nom également unique. Ce nom n'est pas obligatoire, les applications utilisant toujours finalement les adresses IP des machines. lls sont là pour faciliter la vie des utilisateurs. Ainsi il est plus facile, avec un navigateur, de demander l'URL http://www.ibm.com que l'URL http://129.42.17.99 bien que les deux méthodes soient possibles.

Une machine peut avoir plusieurs adresses IP si elle est physiquement connectée à plusieurs réseaux en même temps. Elle a alors une adresse IP sur chaque réseau.

Une adresse IP peut être représentée de deux façons dans .NET :

  • sous la forme d'une chaîne de caractères "I1.I2.I3.I4" ou "[I1.I2.I3.I4.I5.I6.I7.I8]"
  • sous la forme d'un objet de type IPAddress

La classe IPAddress

Parmi les méthodes M, propriétés P et constantes C de la classe IPAddress, on trouve les suivantes :

AddressFamily AddressFamily | P | famille de l'adresse IP. Le type AddressFamily est une énumération. Les deux valeurs courantes sont : AddressFamily.InterNetwork : pour une adresse IPv4 AddressFamily.InterNetworkV6 : pour une adresse IPv6
IPAddress Any | C | l'adresse IP "0.0.0.0". Lorsqu'un service est associé à cette adresse, cela signifie qu'il accepte des clients sur toutes les adresses IP de la machine sur laquelle il opère.
IPAddress LoopBack | C | l'adresse IP "127.0.0.1". Appelée "adresse de boucle". Lorsqu'un service est associé à cette adresse, cela signifie qu'il n'accepte que les clients qui sont sur la même machine que lui.
IPAdress None | C | l'adresse IP "255.255.255.255". Lorsqu'un service est associé à cette adresse, cela signifie qu'il n'accepte aucun client.
bool TryParse(string ipString, out IPAddress address) | M | essaie de passer l'adresse IP ipString de forme "I1.I2.I3.I4" sous la forme d'un objet IPAddress address . Rend true si l'opération a réussi.
bool IsLoopBack | M | rend true si l'adresse IP est "127.0.0.1"
string ToString() | M | rend l'adrresse IP sous la forme "I1.I2.I3.I4" ou "[I1.I2.I3.I4.I5.I6.I7.I8]"

L'association adresse IP <--> nomMachine est assurée par un service distribué de l'internet appelé DNS (Domain Name System). Les méthodes statiques de la classe Dns permettent de faire l'association adresse IP <--> nomMachine :

GetHostEntry (string hostNameOrdAddress) | rend une adresse IPHostEntry à partir d'une adresse IP sous la forme d'une chaîne ou à partir d'un nom de machine. Lance une exception si la machine ne peut être trouvée.
GetHostEntry (IPAddress ip) | rend une adresse IPHostEntry à partir d'une adresse IP de type IPAddress. Lance une exception si la machine ne peut être trouvée.
string GetHostName() | rend le nom de la machine sur laquelle s'exécute le programme qui joue cette instruction
IPAddress[] GetHostAddresses(string hostNameOrdAddress) | rend les adresses IP de la machine identifiée par son nom ou l'une de ses adresses IP.

Une instance IPHostEntry encapsule les adresses IP, les alias et le nom d'une machine. Le type IPHostEntry est le suivant :

IPAddress[] AddressList | P | tableau des adresses IP de la machine
String[] Aliases | P | les alias DNS de la machine. Ceux-ci sont les noms correspondant aux différentes adresses IP de la machine.
string HostName | P | le nom d'hôte principal de la machine

Considérons le programme suivant qui affiche le nom de la machine sur laquelle il s'exécute puis de façon interactive donne les correspondances adresse IP <--> nom Machine :

Cacher Sélectionnez
  1. using System;
  2. using System.Net;
  3.
  4. namespace Chap9 {
  5.     class Program {
  6.         static void Main(string[] args) {
  7.             // affiche le nom de la machine locale
  8.             // puis donne interactivement des infos sur les machines réseau
  9.             // identifiées par un nom ou une adresse IP
  10.
  11.             // machine locale
  12.             Console.WriteLine("Machine Locale= {0}" ,Dns.GetHostName());
  13.
  14.             // question-réponses interactives
  15.             string machine;
  16.             IPHostEntry ipHostEntry;
  17.             while (true) {
  18.                 // saisie du nom ou de l'adresse IP de la machine recherchée
  19.                 Console.Write("Machine recherchée (rien pour arrêter) : ");
  20.                 machine = Console.ReadLine().Trim().ToLower();
  21.                 // fini ?
  22.                 if (machine == "") return;
  23.                 // gestion exception
  24.                 try {
  25.                     // recherche machine
  26.                     ipHostEntry = Dns.GetHostEntry(machine);
  27.                     // le nom de la machine
  28.                     Console.WriteLine("Machine : " + ipHostEntry.HostName);
  29.                     // les adresses IP de la machine
  30.                     Console.Write("Adresses IP : {0}" , ipHostEntry.AddressList[0]);
  31.                     for (int i = 1; i < ipHostEntry.AddressList.Length; i++) {
  32.                         Console.Write(", {0}" , ipHostEntry.AddressList[i]);
  33.                     }
  34.                     Console.WriteLine();
  35.                     // les alias de la machine
  36.                     if (ipHostEntry.Aliases.Length != 0) {
  37.                         Console.Write("Alias : {0}" , ipHostEntry.Aliases[0]);
  38.                         for (int i = 1; i < ipHostEntry.Aliases.Length; i++) {
  39.                             Console.Write(", {0}" , ipHostEntry.Aliases[i]);
  40.                         }
  41.                         Console.WriteLine();
  42.                     }
  43.                 } catch {
  44.                     // la machine n'existe pas
  45.                     Console.WriteLine("Impossible de trouver la machine [{0}]",machine);
  46.                 }
  47.             }
  48.         }
  49.     }
  50. }

L'exécution donne les résultats suivants :

Cacher Sélectionnez
  1. Machine Locale= LISA-AUTO2005A
  2. Machine recherchée (rien pour arrêter) : localhost
  3. Machine : LISA-AUTO2005A
  4. Adresses IP : 127.0.0.1
  5. Machine recherchée (rien pour arrêter) : 127.0.0.1
  6. Machine : LISA-AUTO2005A
  7. Adresses IP : 127.0.0.1
  8. Machine recherchée (rien pour arrêter) : istia.univ-angers.fr
  9. Machine : istia.univ-angers.fr
  10. Adresses IP : 193.49.146.171
  11. Machine recherchée (rien pour arrêter) : 193.49.146.171
  12. Machine : istia.istia.univ-angers.fr
  13. Adresses IP : 193.49.146.171
  14. Machine recherchée (rien pour arrêter) : xx
  15. Impossible de trouver la machine [xx]

XII-C. Les bases de la programmation internet

XII-C-1. Généralités

Considérons la communication entre deux machines distantes A et B :

Image non disponible

Lorsque une application AppA d'une machine A veut communiquer avec une application AppB d'une machine B de l'Internet, elle doit connaître plusieurs choses :

  • l'adresse IP ou le nom de la machine B
  • le numéro du port avec lequel travaille l'application AppB . En effet la machine B peut supporter de nombreuses applications qui travaillent sur l'Internet. Lorsqu'elle reçoit des informations provenant du réseau, elle doit savoir à quelle application sont destinées ces informations. Les applications de la machine B ont accès au réseau via des guichets appelés également des ports de communication. Cette information est contenue dans le paquet reçu par la machine B afin qu'il soit délivré à la bonne application.
  • les protocoles de communication compris par la machine B. Dans notre étude, nous utiliserons uniquement les protocoles TCP-IP.
  • le protocole de dialogue accepté par l'application AppB . En effet, les machines A et B vont se "parler". Ce qu'elles vont dire va être encapsulé dans les protocoles TCP-IP. Néanmoins, lorsqu'au bout de la chaîne, l'application AppB va recevoir l'information envoyée par l'applicaton AppA , il faut qu'elle soit capable de l'interpréter. Ceci est analogue à la situation où deux personnes A et B communiquent par téléphone : leur dialogue est transporté par le téléphone. La parole va être codée sous forme de signaux par le téléphone A, transportée par des lignes téléphoniques, arriver au téléphone B pour y être décodée. La personne B entend alors des paroles. C'est là qu'intervient la notion de protocole de dialogue : si A parle français et que B ne comprend pas cette langue, A et B ne pourront dialoguer utilement.

Aussi les deux applications communicantes doivent -elles être d'accord sur le type de dialogue qu'elles vont adopter. Par exemple, le dialogue avec un service ftp n'est pas le même qu'avec un service pop : ces deux services n'acceptent pas les mêmes commandes. Elles ont un protocole de dialogue différent.

XII-C-2. Les caractéristiques du protocole TCP

Nous n'étudierons ici que des communications réseau utilisant le protocole de transport TCP. Rappelons ici, les caractéristiques de celui-ci :

  • Le processus qui souhaite émettre établit tout d'abord une connexion avec le processus destinataire des informations qu'il va émettre. Cette connexion se fait entre un port de la machine émettrice et un port de la machine réceptrice. Il y a entre les deux ports un chemin virtuel qui est ainsi créé et qui sera réservé aux deux seuls processus ayant réalisé la connexion.
  • Tous les paquets émis par le processus source suivent ce chemin virtuel et arrivent dans l'ordre où ils ont été émis
  • L'information émise a un aspect continu. Le processus émetteur envoie des informations à son rythme. Celles-ci ne sont pas nécessairement envoyées tout de suite : le protocole TCP attend d'en avoir assez pour les envoyer. Elles sont stockées dans une structure appelée segment TCP . Ce segment une fois rempli sera transmis à la couche IP où il sera encapsulé dans un paquet IP.
  • Chaque segment envoyé par le protocole TCP est numéroté. Le protocole TCP destinataire vérifie qu'il reçoit bien les segments en séquence. Pour chaque segment correctement reçu, il envoie un accusé de réception à l'expéditeur.
  • Lorsque ce dernier le reçoit, il l'indique au processus émetteur. Celui-ci peut donc savoir qu'un segment est arrivé à bon port.
  • Si au bout d'un certain temps, le protocole TCP ayant émis un segment ne reçoit pas d'accusé de réception, il retransmet le segment en question, garantissant ainsi la qualité du service d'acheminement de l'information.
  • Le circuit virtuel établi entre les deux processus qui communiquent est full-duplex : cela signifie que l'information peut transiter dans les deux sens. Ainsi le processus destination peut envoyer des accusés de réception alors même que le processus source continue d'envoyer des informations. Cela permet par exemple au protocole TCP source d'envoyer plusieurs segments sans attendre d'accusé de réception. S'il réalise au bout d'un certain temps qu'il n'a pas reçu l'accusé de réception d'un certain segment n° n , il reprendra l'émission des segments à ce point.

XII-C-3. La relation client-serveur

Souvent, la communication sur Internet est dissymétrique : la machine A initie une connexion pour demander un service à la machine B : il précise qu'il veut ouvrir une connexion avec le service SB1 de la machine B. Celle-ci accepte ou refuse. Si elle accepte, la machine A peut envoyer ses demandes au service SB1. Celles-ci doivent se conformer au protocole de dialogue compris par le service SB1. Un dialogue demande-réponse s'instaure ainsi entre la machine A qu'on appelle machine cliente et la machine B qu'on appelle machine serveur. L'un des deux partenaires fermera la connexion.

XII-C-4. Architecture d'un client

L'architecture d'un programme réseau demandant les services d'une application serveur sera la suivante :

Cacher Sélectionnez
  1. ouvrir la connexion avec le service SB1 de la machine B
  2. si réussite alors
  3.     tant que ce n'est pas fini
  4.         préparer une demande
  5.         l'émettre vers la machine B
  6.         attendre et récupérer la réponse
  7.         la traiter
  8.     fin tant que
  9. finsi
  10. fermer la connexion

XII-C-5. Architecture d'un serveur

L'architecture d'un programme offrant des services sera la suivante :

Cacher Sélectionnez
  1. ouvrir le service sur la machine locale
  2. tant que le service est ouvert
  3.     se mettre à l'écoute des demandes de connexion sur un port dit port d'écoute
  4.     lorsqu'il y a une demande, la faire traiter par une autre tâche sur un autre port dit port de service
  5. fin tant que

Le programme serveur traite différemment la demande de connexion initiale d'un client de ses demandes ultérieures visant à obtenir un service. Le programme n'assure pas le service lui-même. S'il le faisait, pendant la durée du service il ne serait plus à l'écoute des demandes de connexion et des clients ne seraient alors pas servis. Il procède donc autrement : dès qu'une demande de connexion est reçue sur le port d'écoute puis acceptée, le serveur crée une tâche chargée de rendre le service demandé par le client. Ce service est rendu sur un autre port de la machine serveur appelé port de service. On peut ainsi servir plusieurs clients en même temps.

Une tâche de service aura la structure suivante :

Cacher Sélectionnez
  1. tant que le service n'a pas été rendu totalement
  2.         attendre une demande sur le port de service
  3.         lorsqu'il y en a une, élaborer la réponse
  4.         transmettre la réponse via le port de service
  5. fin tant que
  6. libérer le port de service

XII-D. Découvrir les protocoles de communication de l'internet

XII-D-1. Introduction

Lorsqu'un client s'est connecté à un serveur, s'établit ensuite un dialogue entre-eux. La nature de celui-ci forme ce qu'on appelle le protocole de communication du serveur. Parmi les protocoles les plus courants de l'internet on trouve les suivants :

  • HTTP : HyperText Transfer Protocol - le protocole de dialogue avec un serveur web (serveur HTTP)
  • SMTP : Simple Mail Transfer Protocol - le protocole de dialogue avec un serveur d'envoi de courriers électroniques (serveur SMTP)
  • POP : Post Office Protocol - le protocole de dialogue avec un serveur de stockage du courrier électronique (serveur POP). Il s'agit là de récupérer les courriers électroniques reçus et non d'en envoyer.
  • FTP : File Transfer Protocol - le protocole de dialogue avec un serveur de stockage de fichiers (serveur FTP).

Tous ces protocoles ont la particularité d'être des protocoles à lignes de texte : le client et le serveur s'échangent des lignes de texte. Si on a un client capable de :

  • créer une connexion avec un serveur Tcp
  • afficher à la console les lignes de texte que le serveur lui envoie
  • envoyer au serveur les lignes de texte qu'un utilisateur saisirait

alors on est capable de dialoguer avec un serveur Tcp ayant un protocole à lignes de texte pour peu qu'on connaisse les règles de ce protocole.

Le programme telnet qu'on trouve sur les machines Unix ou Windows est un tel client. Sur les machines Windows, on trouve également un outil appelé putty et c'est lui que nous allons utiliser ici. putty est téléchargeable à l'adresse [http://www.putty.org/]. C'est un exécutable (.exe) directement utilisable. Nous le configurerons de la façon suivante :

Image non disponible
  • [1] : l'adresse IP du serveur Tcp auquel on veut se connceter ou son nom
  • [2] : le port d'écoute du serveur Tcp
  • [3] : prendre le mode Raw qui désigne une connexion Tcp brute.
  • [4] : prendre le mode Never pour empêcher la fenêtre du client putty de se fermer si le serveur ferme la connexion.
  • [6,7] : nombre de colonnes / lignes de la console
  • [5] : le nombre maximal de lignes conservées en mémoire. Un serveur HTTP peut envoyer beaucoup de lignes. Il faut pouvoir "scroller" dessus.
Image non disponible
  • [8,9] : pour conserver les paramètres précédents, donner un nom à la configuration [8] et la sauvegarder [9].
  • [11,12] : pour récupérer une configuration sauvegardée, la sélectionner [11] et la charger [12].

Avec cet outil ainsi configuré, découvrons quelques protocoles TCP.

XII-D-2. Le protocole HTTP (HyperText Transfer Protocol)

Connectons [1] notre client TCP sur le serveur web de la machine istia.univ-angers.fr [2], port 80 [3] :

Image non disponible

Dans la console de putty , nous construisons le dialogue suivant :

Cacher Sélectionnez
  1. GET / HTTP/1.1
  2. Host: istia.univ-angers.fr:80
  3. Connection: close
  4.
  5. HTTP/1.1 200 OK
  6. Date: Sat, 03 May 2008 07:53:47 GMT
  7. Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
  8. X-Powered-By: PHP/4.4.4-8+etch4
  9. Set-Cookie: fe_typo_user=0d2e64b317; path=/
  10. Connection: close
  11. Transfer-Encoding: chunked
  12. Content-Type: text/html;charset=iso-8859-1
  13.
  14. 693f
  15. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"                                                                        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  16.          <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr_FR" lang="fr_FR">
  17. ....
  18.          </html>
  19. 0
  • les lignes 1-4 sont la demande du client, tapée au clavier
  • les lignes 5-19 sont la réponse du serveur
  • ligne 1 : syntaxe GET UrlDocument HTTP/1.1 - nous demandons l'Url /, c.a.d. la racine du site web [istia.univ-angers.fr].
  • ligne 2 : syntaxe Host: machine:port
  • ligne 3 : syntaxe Connection: [mode de la connexion] . Le mode [close] indique au serveur de fermer la connexion une fois qu'il aura envoyée sa réponse. Le mode [Keep-Alive] demande de la laisser ouverte.
  • ligne 4 : ligne vide. Les lignes 1-3 sont appelées entêtes HTTP. Il peut y en avoir d'autres que ceux présentés ici. La fin des entêtes HTTP est signalée avec une ligne vide.
  • lignes 5-13 : les entêtes HTTP de la réponse du serveur - se terminent là également par une ligne vide.
  • lignes 14-19 : le document envoyé par le serveur, ici un document HTML
  • ligne 5 : syntaxe HTTP/1.1 code msg - le code 200 indique que le document demandé a été trouvé.
  • ligne 6 : les date et heure du serveur
  • ligne 7 : identification logiciel assurant le service web - ici un serveur Apache sur un Linux / Debian
  • ligne 8 : le document a été généré dynamiquement par PHP
  • ligne 9 : cookie d'identification du client - si celui-ci veut se faire reconnaître à sa prochaine connexion, il devra renvoyer ce cookie dans ses entêtes HTTP.
  • ligne 10 : indique qu'après avoir servi le document demandé, le serveur fermera la connexion
  • ligne 11 : le document va être transmis par morceaux (chunked) et non d'un seul bloc.
  • ligne 12 : nature du document : ici un document HTML
  • ligne 13 : la ligne vide qui signale la fin des entêtes HTTP du serveur
  • ligne 14 : nombre hexadécimal indiquant le nombre de caractères du 1er bloc du document. Lorsque ce nombre vaudra 0 (ligne 19), le client saura qu'il a reçu tout le document.
  • lignes 15-18 : partie du document reçu.

La connexion a été fermée et le client putty est inactif. Reconnectons-nous [1] et nettoyons l'écran des affichages précédents [2,3] :

Image non disponible

Le dialogue cette fois-ci est le suivant :

Cacher Sélectionnez
  1. GET /inconnu HTTP/1.1
  2. Host: istia.univ-angers.fr:80
  3. Connection: Close
  4.
  5. HTTP/1.1 404 Not Found
  6. Date: Sat, 03 May 2008 08:16:02 GMT
  7. Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
  8. Connection: close
  9. Transfer-Encoding: chunked
  10. Content-Type: text/html; charset=iso-8859-1
  11.
  12. 11a
  13. <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
  14.                                                   <HTML><HEAD>
  15.                                                               <TITLE>404 Not Found</TITLE>
  16.                                                                                           </HEAD><BODY>
  17.                                                                                                        <H1>Not Found</H1>
  18.  The requested URL /inconnu was not found on this server.<P>
  19.                                                             <HR>
  20.                                                                 <ADDRESS>Apache/1.3.34 Server at www.istia.univ-angers.fr Port 80</ADDRESS>
  21.                    </BODY></HTML>
  22.
  23. 0
  • ligne 1 : on a demandé un document inexistant
  • ligne 5 : le serveur HTTP a répondu avec le code 404 signifiant que le document demandé n'a pas été trouvé.

Si on demande ce document avec un navigateur Firefox :

Image non disponible

Si nous demandons à voir le code source [Affichage/Code source] :

Cacher Sélectionnez
  1. <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
  2. <HTML><HEAD>
  3. <TITLE>404 Not Found</TITLE>
  4. </HEAD><BODY>
  5. <H1>Not Found</H1>
  6. The requested URL /inconnu was not found on this server.<P>
  7. <HR>
  8. <ADDRESS>Apache/1.3.34 Server at www.istia.univ-angers.fr Port 80</ADDRESS>
  9. </BODY></HTML>

Nous obtenons les lignes 13-22 reçues par notre client putty . L'intérêt de celui-ci est de nous montrer en plus, les entêtes HTTP de la réponse. Il est également possible d'avoir ceux-ci avec Firefox.

XII-D-3. Le protocole SMTP (Simple Mail Transfer Protocol)

Image non disponible

Les serveurs SMTP opèrent en général sur le port 25 [2]. On se connecte sur le serveur [1]. Ici, il faut en général prendre un serveur

appartenant au même domaine IP que la machine car le plus souvent les serveurs SMTP sont configurés pour n'accepter que les demandes des machines appartenant au même domaine qu'eux. Par ailleurs, assez souvent également, les pare-feu ou antivirus des machines personnelles sont configurés pour ne pas accepter de connexion vers le port 25 d'une machine extérieure. Il peut être alors nécessaire de reconfigurer [3] ce pare-feu ou antivirus.

Le dialogue SMTP dan la fenêtre du client putty est le suivant :

Cacher Sélectionnez
  1. 220 neuf-infra-smtp-out-sp604001av.neufgp.fr neuf telecom Service relais mail ready
  2. HELO istia.univ-angers.fr
  3. 250 neuf-infra-smtp-out-sp604002av.neufgp.fr hello [84.100.189.193], Banniere OK , pret pour envoyer un mail
  4. mail from: @expéditeur
  5. 250 2.1.0 <@expéditeur> sender ok
  6. rcpt to: @destinataire
  7. 250 2.1.5 <@destinataire> destinataire ok
  8. data
  9. 354 enter mail, end with "." on a line by itself
  10. ligne1
  11. ligne2
  12. .
  13. 250 2.0.0 LwiU1Z00V4AoCxw0200000 message ok
  14. quit
  15. 221 2.0.0 neuf-infra-smtp-out-sp604002av.neufgp.fr neuf telecom closing connection

Ci-dessous (D) est une demande du client, (R) une réponse du serveur.

  • ligne 1 : (R) message d'accueil du serveur SMTP
  • ligne 2 : (D) commande HELO pour dire bonjour
  • ligne 3 : (R) réponse du serveur
  • ligne 4 : (D) adresse expéditeur, par exemple mail from: someone@gmail.com
  • ligne 5 : (R) réponse du serveur
  • ligne 6 : (D) adresse destinataire, par exemple rcpt to: someoneelse@gmail.com
  • ligne 7 : (R) réponse du serveur
  • ligne 8 : (D) signale le début du message
  • ligne 9 : (R) réponse du serveur
  • lignes 10-12 : (D) le message à envoyer terminé par une ligne contenant uniquement un point.
  • ligne 13 : (R) réponse du serveur
  • ligne 14 : (D) le client signale qu'il a terminé
  • ligne 15 : (R) réponse du serveur qui ensuite ferme la connexion

XII-D-4. Le protocole POP (Post Office Protocol)

Image non disponible

Les serveurs POP opèrent en général sur le port 110 [2]. On se connecte sur le serveur [1]. Le dialogue POP dans la fenêtre du client putty est le suivant :

Cacher Sélectionnez
  1. +OK Hello there.
  2. user xx
  3. +OK Password required.
  4. pass yy
  5. +OK logged in.
  6. list
  7. +OK POP3 clients that break here, they violate STD53.
  8. 1 10105
  9. 2 55875
  10. ...
  11. 64 1717
  12. .
  13. retr 64
  14. +OK 1717 octets follow.
  15. Return-Path: <xx@neuf.fr>
  16. X-Original-To: xx@univ-angers.fr
  17. Delivered-To: xx@univ-angers.fr
  18. ....
  19. Date: Sat,  3 May 2008 10:59:25 +0200 (CEST)
  20. From: xx@neuf.fr
  21. To: undisclosed-recipients:;
  22.
  23. ligne1
  24. ligne2
  25. .
  26. quit
  27. +OK Bye-bye.
  • ligne 1 : (R) message de bienvenue du serveur POP
  • ligne 2 : (D) le client donne son identifiant POP, c.a.d. le login avec lequel il lit son courrier
  • ligne 3 : (R) la réponse du serveur
  • ligne 4 : (D) le mot de passe du client
  • ligne 5 : (R) la réponse du serveur
  • ligne 6 : (D) le client demande la liste de ses courriers
  • lignes 7-12 : (R) la liste des messages dans la boîte à lettre du client, sous la forme [N° du message taille en octets du message]
  • ligne 13 : (D) on demande le message n° 64
  • lignes 14-25 : (R) le message n° 64 avec lignes 15-22, les entêtes du message, et lignes 23-24 le corps du message.
  • ligne 26 : (D) le client indique qu'il a fini
  • ligne 27 : (R) réponse du serveur qui va ensuite fermer la connexion.

XII-D-5. Le protocole FTP (File Transfer Protocol)

Le protocole FTP est plus complexe que ceux présentés précédemment. Pour découvrir les lignes de texte échangées entre le client et le serveur, on pourra utiliser un outil tel que FileZilla [http://www.filezilla.fr/].

Image non disponible

Filezilla est un client FTP offrant une interface windows pour faire des transferts de fichiers. Les actions de l'utilisateur sur l'interface windows sont traduites en commandes FTP qui sont loguées en [1]. C'est une bonne façon de découvrir les commandes du protocole FTP.

XII-E. Les classes .NET de la programmation internet

XII-E-1. Choisir la classe adaptée

Le framework .NET offre différentes classes pour travailler avec le réseau :

Image non disponible
  • la classe Socket est celle qui opère le plus près du réseau. Elle permet de gérer finement la connexion réseau. Le terme socket désigne une prise de courant. Le terme a été étendu pour désigner une prise de réseau logicielle. Dans une communication TCP-IP entre deux machines A et B, ce sont deux sockets qui communiquent entre-eux. Une application peut travailler directement avec les sockets . C'est le cas de l'application A ci-dessus. Un socket peut être un socket client ou serveur .
  • si on souhaite travailler à un niveau moins fin que celui de la classe Socket , on pourra utiliser les classes
  • TcpClient pour créer un client Tcp
  • TcpListener pour créer un serveur Tcp

Ces deux classes offrent à l'application qui les utilisent, une vue plus simple de la communication réseau en gérant pour elle les détails techniques de gestion des sockets.

  • .NET offre des classes spécifiques à certains protocoles :
  • la classe SmtpClient pour gérer le protocole SMTP de communication avec un serveur SMTP d'envoi de courriers électroniques
  • la classe WebClient pour gérer les protocoles HTTP ou FTP de communication avec un serveur web.

On retiendra que la classe Socket est suffisante en elle-même pour gérer toute communication tcp-ip mais on cherchera avant tout à utiliser les classes de plus haut niveau afin de faciliter l'écriture de l'application tcp-ip.

XII-E-2. La classe TcpClient

La classe TcpClient est la classe qui convient dans la plupart des cas pour créer le client d'un service TCP. Elle a parmi ses constructeurs C, méthodes M et propriétés P, les suivants :

TcpClient(string hostname, int port) | C | crée une liaison tcp avec le service opérant sur le port indiqué ( port ) de la machine indiquée ( hostname ). Par exemple new TcpClient("istia.univ-angers.fr",80) pour se connecter au port 80 de la machine istia.univ-angers.fr
Socket Client | P | le socket utilisé par le client pour communiquer avec le serveur.
NetworkStream GetStream() | M | obtient un flux de lecture et d'écriture vers le serveur. C'est ce flux qui permet les échanges client-serveur.
void Close() | M | ferme la connexion. Le socket et le flux NetworkStream sont également fermés
bool Connected() | P | vrai si la connexion a été établie

La classe NetworkStream représente le flux réseau entre le client et le serveur. Elle est dérivée de la classe Stream . Beaucoup d'applications client-serveur échangent des lignes de texte terminées par les caractères de fin de ligne "\r\n". Aussi est-il intéressant d'utiliser des objets StreamReader et StreamWriter pour lire et écrire ces lignes dans le flux réseau. Ainsi si une machine M1 a établi une liaison avec une machine M2 à l'aide d'un objet TcpClient client1 et qu'elles échangent des lignes de texte, elle pourra créer ses flux de lecture et écriture de la façon suivante :

Cacher Sélectionnez
  1. StreamReader in1=new StreamReader(client1.GetStream());
  2. StreamWriter out1=new StreamWriter(client1.GetStream());
  3. out1.AutoFlush=true;

L'instruction

Cacher Sélectionnez
  1. out1.AutoFlush=true;

signifie que le flux d'écriture de client1 ne transitera pas par un buffer intermédiaire mais ira directement sur le réseau. Ce point est important. En général lorsque client1 envoie une ligne de texte à son partenaire il en attend une réponse. Celle-ci ne viendra jamais si la ligne a été en réalité bufferisée sur la machine M1 et jamais envoyée à la machine M2.

Pour envoyer une ligne de texte à la machine M2, on écrira :

Cacher Sélectionnez
  1. client1.WriteLine("un texte");

Pour lire la réponse de M2, on écrira :

Cacher Sélectionnez
  1. string réponse=client1.ReadLine();

Nous avons maintenant les éléments pour écrire l'architecture de base d'un client internet ayant le protocole de communication basique suivant avec le serveur :

  • le client envoie une demande contenue dans une unique ligne
  • le serveur envoie une réponse contenue dans une unique ligne
Cacher Sélectionnez
  1. using System;
  2. using System.IO;
  3. using System.Net.Sockets;
  4.
  5. namespace ... {
  6.     class ... {
  7.         static void Main(string[] args) {
  8.             ...
  9.             try {
  10.                 // on se connecte au service
  11.                 using (TcpClient tcpClient = new TcpClient(serveur, port)) {
  12.                     using (NetworkStream networkStream = tcpClient.GetStream()) {
  13.                         using (StreamReader reader = new StreamReader(networkStream)) {
  14.                             using (StreamWriter writer = new StreamWriter(networkStream)) {
  15.                                 // flux de sortie non bufferisé
  16.                                 writer.AutoFlush = true;
  17.                                 // boucle demande - réponse
  18.                                 while (true) {
  19.                                     // la demande vient du clavier
  20.                                     Console.Write("Demande (bye pour arrêter) : ");
  21.                                     demande = Console.ReadLine();
  22.                                     // fini ?
  23.                                     if (demande.Trim().ToLower() == "bye")
  24.                                         break;
  25.                                     // on envoie la demande au serveur
  26.                                     writer.WriteLine(demande);
  27.                                     // on lit la réponse du serveur
  28.                                     réponse = reader.ReadLine();
  29.                                     // on traite la réponse
  30.                                     ...
  31.                                 }
  32.                             }
  33.                         }
  34.                     }
  35.                 }
  36.             } catch (Exception e) {
  37.                 // erreur
  38.                 ...
  39.             }
  40.         }
  41.     }
  42. }
  • ligne 11 : création connexion du client - la clause using assure que les ressources liées à celle-ci seront libérées à la sortie du using .
  • ligne 12 : ouverture du flux réseau dans une clause using
  • ligne 13 : création et exploitation du flux de lecture dans une clause using
  • ligne 14 : création et exploitation du flux d'écriture dans une clause using
  • ligne 16 : ne pas bufferiser le flux de sortie
  • lignes 18-31 : le cycle demande client / réponse serveur
  • ligne 26 : le client envoie sa demande au serveur
  • ligne 28 : le client attend la réponse du serveur. C'est une opération bloquante comme celle de la lecture au clavier. L'attente se termine par l'arrivée d'une chaîne terminée par "\n" ou bien par une fin de flux. Celle-ci se produira si le serveur ferme la connexion qu'il a ouverte avec le client.

XII-E-3. La classe TcpListener

La classe TcpListener est la classe qui convient dans la plupart des cas pour créer un service TCP. Elle a parmi ses constructeurs C, méthodes M et propriétés P, les suivants :

TcpListener(int port) | C | crée un service TCP qui va attendre (listen) les demandes des clients sur un port passé en paramètre (port) appelé port d'écoute. Si la machine est connectée à plusieurs réseaux IP, le service écoute sur chacun des réseaux.
TcpListener(IPAddress ip, int port) | C | idem mais l'écoute n'a lieu que sur l'adresse ip précisée.
void Start() | M | lance l'écoute des demandes clients
TcpClient AcceptTcpClient() | M | accepte la demande d'un client. Ouvre alors une nouvelle connexion avec celui-ci, appelée connexion de service. Le port utilisé côté serveur est aléatoire et choisi par le système. On l'appelle le port de service. AcceptTcpClient rend comme résultat l'objet TcpClient associé côté serveur à la connexion de service.
void Stop() | M | arrête d'écouter les demandes clients
Socket Server | P | le socket d'écoute du serveur

La structure de base d'un serveur TCP qui échangerait avec ses clients selon le protocole suivant :

  • le client envoie une demande contenue dans une unique ligne
  • le serveur envoie une réponse contenue dans une unique ligne

pourrait ressembler à ceci :

Cacher Sélectionnez
  1. using System;
  2. using System.IO;
  3. using System.Net.Sockets;
  4. using System.Threading;
  5. using System.Net;
  6.
  7. namespace ... {
  8.     public class ... {
  9.             ...
  10.             // on crée le service d'écoute
  11.             TcpListener ecoute = null;
  12.             try {
  13.                 // on crée le service - il écoutera sur toutes les interfaces réseau de la machine
  14.                 ecoute = new TcpListener(IPAddress.Any, port);
  15.                 // on le lance
  16.                 ecoute.Start();
  17.                 // boucle de service
  18.                 TcpClient tcpClient = null;
  19.                 // boucle infinie - sera arrêtée par Ctrl-C
  20.                 while (true) {
  21.                     // attente d'un client
  22.                     tcpClient = ecoute.AcceptTcpClient();
  23.                     // le service est assuré par une autre tâche
  24.                     ThreadPool.QueueUserWorkItem(Service, tcpClient);
  25.                     // client suivant
  26.                 }
  27.             } catch (Exception ex) {
  28.                 // on signale l'erreur
  29.                 ...
  30.             } finally {
  31.                 // fin du service
  32.                 ecoute.Stop();
  33.             }
  34.         }
  35.
  36.         // -------------------------------------------------------
  37.         // assure le service à un client
  38.         public static void Service(Object infos) {
  39.             // on récupère le client qu'il faut servir
  40.             Client client = infos as Client;
  41.             // exploitation liaison TcpClient
  42.             try {
  43.                 using (TcpClient tcpClient = client.CanalTcp) {
  44.                     using (NetworkStream networkStream = tcpClient.GetStream()) {
  45.                         using (StreamReader reader = new StreamReader(networkStream)) {
  46.                             using (StreamWriter writer = new StreamWriter(networkStream)) {
  47.                                 // flux de sortie non bufferisé
  48.                                 writer.AutoFlush = true;
  49.                                 // boucle lecture demande/écriture réponse
  50.                                 bool fini=false;
  51.                                 while (! fini) != null) {
  52.                                     // attente demande client - opération bloquante
  53.                                     demande=reader.ReadLine();
  54.                                     // préparation réponse
  55.                                     réponse=...;
  56.                                     // envoi réponse au client
  57.                                     writer.WriteLine(réponse);
  58.                                     // demande suivante
  59.                                 }
  60.                             }
  61.                         }
  62.                     }
  63.                 }
  64.             } catch (Exception e) {
  65.                 // erreur
  66.                 ...
  67.             } finally {
  68.                 // fin client
  69.                 ...
  70.             }
  71.         }
  72.     }
  73. }
  • ligne 14 : le service d'écoute est créé pour un port donné et une adresse IP donnée. Il faut se rappeler ici qu'une machine a au moins deux adresses IP : l'adresse "127.0.0.1" qui est son adresse de bouclage sur elle-même et l'adresse "I1.I2.I3.I4" qu'elle a sur le réseau auquel elle est connectée. Elle peut avoir d'autres adresses IP si elle connectée à plusieurs réseaux IP. IPAddress.Any désigne toutes les adresses IP d'une machine.
  • ligne 16 : le service d'écoute démarre. Auparavant il avait été créé mais il n'écoutait pas encore. Ecouter signifie attendre les demandes des clients.
  • lignes 20-26 : la boucle attente demande client / service client répétée pour chaque nouveau client
  • ligne 22 : la demande d'un client est acceptée. La méthode AcceptTcpClient rend une instance TcpClient dite de service :
  • le client a fait sa demande avec sa propre instance TcpClient côté client que nous appellerons TcpClientDemande
  • le serveur accepte cette demande avec AcceptTcpClient . Cette méthode crée une instance TcpClient côté serveur, que nous appellerons TcpClientService . On a alors une connexion Tcp ouverte avec aux deux bouts les instances TcpClientDemande <--> TcpClientService .
  • la communication client / serveur qui prend place ensuite se fait sur cette connexion. Le service d'écoute n'intervient plus.
  • ligne 24 : afin que le serveur puisse traiter plusieurs clients à la fois, le service est assuré par des threads, 1 thread par client.
  • ligne 32 : le service d'écoute est fermé
  • ligne 38 : la méthode exécutée par le thread de service à un client. Elle reçoit en paramètre l'instance TcpClient déjà connectée au client qui doit être servi.
  • lignes 38-71 : on retrouve un code similaire à celui du client Tcp basique étudié précédemment.

XII-F. Exemples de clients / serveurs TCP

XII-F-1. Un serveur d'écho

Nous nous proposons d'écrire un serveur d'écho qui sera lancé depuis une fenêtre DOS par la commande :

ServeurEcho port

Le serveur officie sur le port passé en paramètre. Il se contente de renvoyer au client la demande que celui-ci lui a envoyée. Le programme est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.IO;
  3. using System.Net.Sockets;
  4. using System.Threading;
  5. using System.Net;
  6.
  7. // appel : serveurEcho port
  8. // serveur d'écho
  9. // renvoie au client la ligne que celui-ci lui a envoyée
  10.
  11. namespace Chap9 {
  12.     public class ServeurEcho {
  13.         public const string syntaxe = "Syntaxe : [serveurEcho] port";
  14.
  15.         // programme principal
  16.         public static void Main(string[] args) {
  17.
  18.             // y-a-t-il un argument ?
  19.             if (args.Length != 1) {
  20.                 Console.WriteLine(syntaxe);
  21.                 return;
  22.             }
  23.             // cet argument doit être entier >0
  24.             int port = 0;
  25.             if (!int.TryParse(args[0], out port) || port<=0) {
  26.                 Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine);
  27.                 return;
  28.             }
  29.             // on crée le service d'écoute
  30.             TcpListener ecoute = null;
  31.             int numClient = 0;    // n° client suivant
  32.             try {
  33.                 // on crée le service - il écoutera sur toutes les interfaces réseau de la machine
  34.                 ecoute = new TcpListener(IPAddress.Any, port);
  35.                 // on le lance
  36.                 ecoute.Start();
  37.                 // suivi
  38.                 Console.WriteLine("Serveur d'écho lancé sur le port {0}", ecoute.LocalEndpoint);
  39.                 // threads de service
  40.                 ThreadPool.SetMinThreads(10, 10);
  41.                 ThreadPool.SetMaxThreads(10, 10);
  42.                 // boucle de service
  43.                 TcpClient tcpClient = null;
  44.                 // boucle infinie - sera arrêtée par Ctrl-C
  45.                 while (true) {
  46.                     // attente d'un client
  47.                     tcpClient = ecoute.AcceptTcpClient();
  48.                     // le service est assuré par une autre tâche
  49.                     ThreadPool.QueueUserWorkItem(Service, new Client() { CanalTcp = tcpClient, NumClient = numClient });
  50.                     // client suivant
  51.                     numClient++;
  52.                 }
  53.             } catch (Exception ex) {
  54.                 // on signale l'erreur
  55.                 Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
  56.             } finally {
  57.                 // fin du service
  58.                 ecoute.Stop();
  59.             }
  60.         }
  61.
  62.         // -------------------------------------------------------
  63.         // assure le service à un client du serveur d'écho
  64.         public static void Service(Object infos) {
  65.             // on récupère le client qu'il faut servir
  66.             Client client = infos as Client;
  67.             // rend le service au client
  68.             Console.WriteLine("Début de service au client {0}", client.NumClient);
  69.             // exploitation liaison TcpClient
  70.             try {
  71.                 using (TcpClient tcpClient = client.CanalTcp) {
  72.                     using (NetworkStream networkStream = tcpClient.GetStream()) {
  73.                         using (StreamReader reader = new StreamReader(networkStream)) {
  74.                             using (StreamWriter writer = new StreamWriter(networkStream)) {
  75.                                 // flux de sortie non bufferisé
  76.                                 writer.AutoFlush = true;
  77.                                 // boucle lecture demande/écriture réponse
  78.                                 string demande = null;
  79.                                 while ((demande = reader.ReadLine()) != null) {
  80.                                     // suivi console
  81.                                     Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
  82.                                     // écho de la demande vers le client
  83.                                     writer.WriteLine("[{0}]", demande);
  84.                                     // suivi console
  85.                                     Console.WriteLine("---> Client {0} : {1}", client.NumClient, demande);
  86.                                     // le service s'arrête lorsque le client envoie "bye"
  87.                                     if (demande.Trim().ToLower() == "bye")
  88.                                         break;
  89.                                 }
  90.                             }
  91.                         }
  92.                     }
  93.                 }
  94.             } catch (Exception e) {
  95.                 // erreur
  96.                 Console.WriteLine("L'erreur suivante s'est produite lors du service au client {0} : {1}", client.NumClient, e.Message);
  97.             } finally {
  98.                 // fin client
  99.                 Console.WriteLine("Fin du service au client {0}", client.NumClient);
  100.             }
  101.         }
  102.     }
  103.
  104.     // infos client
  105.     internal class Client {
  106.         public TcpClient CanalTcp { get; set; }        // liaison avec le client
  107.         public int NumClient { get; set; }            // n° de client
  108.     }
  109. }

La structure du serveur d'écho est conforme à l'architecture basique des serveurs Tcp exposée précédemment. Nous ne commenterons que la partie "service au client" :

  • ligne 79 : la demande du client est lue
  • ligne 83 : elle est renvoyée au client entourée de crochets
  • ligne 79 : le service s'arrête lorsque le client ferme la connexion

Dans une fenêtre Dos, nous utilisons l'exécutable du projet C# :

Cacher Sélectionnez
  1. ...\Chap9\02\bin\Release>dir
  2.  03/05/2008  11:46             7 168 ServeurEcho.exe
  3.
  4. ...>ServeurEcho 100
  5. Serveur d'écho lancé sur le port 0.0.0.0:100

Nous lançons ensuite deux clients putty que nous connectons au port 100 de la machine localhost :

Image non disponible

L'affichage console du serveur d'écho devient :

Cacher Sélectionnez
  1. Serveur d'écho lancé sur le port 0.0.0.0:100
  2. Début de service au client 0
  3. Début de service au client 1

Le client 1 puis le client 0 envoient les textes suivants :

Image non disponible
  • [1] : le client n° 1
  • [2] : le client n° 0
  • [3] : la console du serveur d'écho
Image non disponible
  • en [4] : le client 1 se déconnecte avec la commande bye .
  • en [5] : le serveur le détecte

Le serveur peut être arrêté par Ctrl-C. Le client n° 0 le détecte alors [6].

XII-F-2. Un client pour le serveur d'écho

Nous écrivons maintenant un client pour le serveur précédent. Il sera appelé de la façon suivante :

ClientEcho nomServeur port

Il se connecte à la machine nomServeur sur le port port puis envoie au serveur des lignes de texte que celui-ci lui renvoie en écho.

Cacher Sélectionnez
  1. using System;
  2. using System.IO;
  3. using System.Net.Sockets;
  4.
  5. namespace Chap9 {
  6.     // se connecte à un serveur d'écho
  7.     // toute ligne tapée au clavier est reçue en écho
  8.     class ClientEcho {
  9.         static void Main(string[] args) {
  10.             // syntaxe
  11.             const string syntaxe = "pg machine port";
  12.
  13.             // nombre d'arguments
  14.             if (args.Length != 2) {
  15.                 Console.WriteLine(syntaxe);
  16.                 return;
  17.             }
  18.
  19.             // on note le nom du serveur
  20.             string serveur = args[0];
  21.
  22.             // le port doit être entier >0
  23.             int port = 0;
  24.             if (!int.TryParse(args[1], out port) || port <= 0) {
  25.                 Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
  26.                 return;
  27.             }
  28.
  29.             // on peut travailler
  30.             string demande = null;        // demande du client
  31.             string réponse = null;        // réponse du serveur
  32.             try {
  33.                 // on se connecte au service
  34.                 using (TcpClient tcpClient = new TcpClient(serveur, port)) {
  35.                     using (NetworkStream networkStream = tcpClient.GetStream()) {
  36.                         using (StreamReader reader = new StreamReader(networkStream)) {
  37.                             using (StreamWriter writer = new StreamWriter(networkStream)) {
  38.                                 // flux de sortie non bufferisé
  39.                                 writer.AutoFlush = true;
  40.                                 // boucle demande - réponse
  41.                                 while (true) {
  42.                                     // la demande vient du clavier
  43.                                     Console.Write("Demande (bye pour arrêter) : ");
  44.                                     demande = Console.ReadLine();
  45.                                     // fini ?
  46.                                     if (demande.Trim().ToLower() == "bye")
  47.                                         break;
  48.                                     // on envoie la demande au serveur
  49.                                     writer.WriteLine(demande);
  50.                                     // on lit la réponse du serveur
  51.                                     réponse = reader.ReadLine();
  52.                                     // on traite la réponse
  53.                                     Console.WriteLine("Réponse : {0}", réponse);
  54.                                 }
  55.                             }
  56.                         }
  57.                     }
  58.                 }
  59.             } catch (Exception e) {
  60.                 // erreur
  61.                 Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
  62.             }
  63.         }
  64.     }
  65. }

La structure de ce client est conforme à l'architecture générale basique proposée pour les clients T cp . Voici les résultats obtenus dans la configuration suivante :

  • le serveur est lancé sur le port 100 dans une fenêtre Dos
  • sur la même machine deux clients sont lancés dans deux autres fenêtres Dos

Dans la fenêtre du client A (n° 0) on a les affichages suivants :

Cacher Sélectionnez
  1. ...\Chap9\03\bin\Release>ClientEcho localhost 100
  2. Demande (bye pour arrêter) : ligne1A
  3. Réponse : [ligne1A]
  4. Demande (bye pour arrêter) : ligne2A
  5. Réponse : [ligne2A]
  6. Demande (bye pour arrêter) :

Dans celle du client B (n° 1) :

Cacher Sélectionnez
  1. ...\Chap9\03\bin\Release>ClientEcho localhost 100
  2. Demande (bye pour arrêter) : ligne1B
  3. Réponse : [ligne1B]
  4. Demande (bye pour arrêter) : ligne2B
  5. Réponse : [ligne2B]
  6. Demande (bye pour arrêter) :

Dans celle du serveur :

Cacher Sélectionnez
  1. ...\Chap9\02\bin\Release>ServeurEcho 100
  2. Serveur d'écho lancé sur le port 0.0.0.0:100
  3. Début de service au client 0
  4. <--- Client 0 : ligne1A
  5. ---> Client 0 : ligne1A
  6. <--- Client 0 : ligne2A
  7. ---> Client 0 : ligne2A
  8. Début de service au client 1
  9. <--- Client 1 : ligne1B
  10. ---> Client 1 : ligne1B
  11. <--- Client 1 : ligne2B
  12. ---> Client 1 : ligne2B

Le client A n° 0 se déconnecte :

Cacher Sélectionnez
  1. Demande (bye pour arrêter) : ligne1A
  2. Réponse : [ligne1A]
  3. ...
  4. Demande (bye pour arrêter) : bye

La console du serveur :

Cacher Sélectionnez
  1. Serveur d'écho lancé sur le port 0.0.0.0:100
  2. ...
  3. Fin du service au client 0

XII-F-3. Un client TCP générique

Nous allons écrire un client Tcp générique qui sera lancé de la façon suivante : ClientTcpGenerique serveur port. Il aura un fonctionnement analogue au client putty mais aura une interface console et ne présentera pas d'option de configuration.

Dans l'application précédente, le protocole du dialogue était connu : le client envoyait une seule ligne et le serveur répondait par une seule ligne. Chaque service a son protocole particulier et on trouve également les situations suivantes :

  • le client doit envoyer plusieurs lignes de texte avant d'avoir une réponse
  • la réponse d'un serveur peut comporter plusieurs lignes de texte

Aussi le cycle envoi d'une unique ligne au serveur / réception d'une unique ligne envoyée par le serveur, ne convient-il pas toujours. Pour gérer les protocoles plus complexes que celui d'écho, le client Tcp générique aura deux threads :

  • le thread principal lira les lignes de texte tapées au clavier et les enverra au serveur.
  • un thread secondaire travaillera en parallèle et sera consacré à la lecture des lignes de texte envoyées par le serveur. Dès qu'il en reçoit une, il l'affiche sur la console. Le thread ne s'arrête que lorsque le serveur clôt la connexion. Il travaille donc en continu.

Le code est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.IO;
  3. using System.Net.Sockets;
  4. using System.Threading;
  5.
  6. namespace Chap9 {
  7.     // reçoit en paramètre les caractéristiques d'un service sous la forme : serveur port
  8.     // se connecte au service
  9.     // envoie au serveur chaque ligne tapée au clavier
  10.     // crée un thread pour lire en continu les lignes de texte envoyées par le serveur
  11.     class ClientTcpGenerique {
  12.         static void Main(string[] args) {
  13.             // syntaxe
  14.             const string syntaxe = "pg serveur port";
  15.
  16.             // nombre d'arguments
  17.             if (args.Length != 2) {
  18.                 Console.WriteLine(syntaxe);
  19.                 return;
  20.             }
  21.
  22.             // on note le nom du serveur
  23.             string serveur = args[0];
  24.
  25.             // le port doit être entier >0
  26.             int port = 0;
  27.             if (!int.TryParse(args[1], out port) || port <= 0) {
  28.                 Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
  29.                 return;
  30.             }
  31.             // on se connecte au service
  32.             TcpClient tcpClient = null;
  33.             try {
  34.                 tcpClient = new TcpClient(serveur, port);
  35.             } catch (Exception ex) {
  36.                 // erreur
  37.                 Console.WriteLine("Impossible de se connecter au service ({0},{1}) : erreur {2}", serveur, port, ex.Message);
  38.                 // fin
  39.                 return;
  40.             }
  41.
  42.             // on lance un thread à part pour lire les lignes de texte envoyées par le serveur
  43.             ThreadPool.QueueUserWorkItem(Receive, tcpClient);
  44.
  45.             // la lecture des commandes clavier se fait dans le thread principal
  46.             Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
  47.             string demande = null;        // demande du client
  48.             try {
  49.                 // on exploite la connexion client
  50.                 using (tcpClient) {
  51.                     // on crée un flux d'écriture vers le serveur
  52.                     using (NetworkStream networkStream = tcpClient.GetStream()) {
  53.                         using (StreamWriter writer = new StreamWriter(networkStream)) {
  54.                             // flux de sortie non bufferisé
  55.                             writer.AutoFlush = true;
  56.                             // boucle demande - réponse
  57.                             while (true) {
  58.                                 demande = Console.ReadLine();
  59.                                 // fini ?
  60.                                 if (demande.Trim().ToLower() == "bye")
  61.                                     break;
  62.                                 // on envoie la demande au serveur
  63.                                 writer.WriteLine(demande);
  64.                             }
  65.                         }
  66.                     }
  67.                 }
  68.             } catch (Exception e) {
  69.                 // erreur
  70.                 Console.WriteLine("L'erreur suivante s'est produite dans le thread principal : {0}", e.Message);
  71.             }
  72.         }
  73.
  74.         // thread de lecture client <-- serveur
  75.         public static void Receive(object infos) {
  76.             // données locales
  77.             string réponse = null;    // réponse du serveur
  78.             // création flux d'entrée
  79.             try {
  80.                 using (TcpClient tcpClient = infos as TcpClient) {
  81.                     using (NetworkStream networkStream = tcpClient.GetStream()) {
  82.                         using (StreamReader reader = new StreamReader(networkStream)) {
  83.                             // boucle lecture en continu des lignes de texte du flux d'entrée
  84.                             while ((réponse = reader.ReadLine()) != null) {
  85.                                 // affichage console
  86.                                 Console.WriteLine("<-- {0}", réponse);
  87.                             }
  88.                         }
  89.                     }
  90.                 }
  91.             } catch (Exception ex) {
  92.                 // erreur
  93.                 Console.WriteLine("Flux de lecture : l'erreur suivante s'est produite : {0}", ex.Message);
  94.             } finally {
  95.                 // on signale la fin du thread de lecture
  96.                 Console.WriteLine("Fin du thread de lecture des réponses du serveur. Si besoin est, arrêtez le thread de lecture console avec la commande bye.");
  97.             }
  98.         }
  99.     }
  100. }
  • ligne 34 : le client se connecte au serveur
  • ligne 43 : un thread de lecture des lignes de texte du serveur est lancé. Il doit exécuter la méthode Receive de la ligne 73. On passe à cette méthode l'instance TcpClient qui a été connectée au serveur.
  • lignes 57-64 : la boucle saisie commande clavier / envoi commande au serveur. La saisie des commandes clavier est assurée par le thread principal.
  • lignes 75-98 : la méthode Receive exécutée par le thread de lecture des lignes de texte. Cette méthode reçoit en paramètre l'instance TcpClient qui a été connectée au serveur.
  • lignes 84-87 : la boucle en continu de lecture des lignes de texte envoyées par le serveur. Elle ne s'arrête que lorsque le serveur clôt la connexion ouverte avec le client.

Voici quelques exemples reprenant ceux utilisés avec le client putty au paragraphe , page . Le client est exécuté dans une console Dos.

Protocole HTTP

Cacher Sélectionnez
  1. ...\Chap9\04\bin\Release>ClientTcpGenerique istia.univ-angers.fr 80
  2. Tapez vos commandes (bye pour arrêter) :
  3. GET /inconnu HTTP/1.1
  4. Host: istia.univ-angers.fr:80
  5. Connection: Close
  6.
  7. <-- HTTP/1.1 404 Not Found
  8. <-- Date: Sat, 03 May 2008 12:35:11 GMT
  9. <-- Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
  10.
  11. <-- Connection: close
  12. <-- Transfer-Encoding: chunked
  13. <-- Content-Type: text/html; charset=iso-8859-1
  14. <--
  15. <-- 11a
  16. <-- <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
  17. <-- <HTML><HEAD>
  18. <-- <TITLE>404 Not Found</TITLE>
  19. <-- </HEAD><BODY>
  20. <-- <H1>Not Found</H1>
  21. <-- The requested URL /inconnu was not found on this server.<P>
  22. <-- <HR>
  23. <-- <ADDRESS>Apache/1.3.34 Server at www.istia.univ-angers.fr Port 80</ADDRESS>
  24. <-- </BODY></HTML>
  25. <--
  26. <-- 0
  27. <--
  28. [Fin du thread de lecture des réponses du serveur]
  29. bye
  30.
  31. ...\Chap9\04\bin\Release>

Le lecteur est invité à relire les explications données au paragraphe , page . Nous ne commentons que ce qui est propre à l'application :

  • ligne 28 : après l'envoi de la ligne 27, le serveur HTTP a fermé la connexion, ce qui a provoqué la fin du thread de lecture. Le thread principal qui lit les commandes tapées au clavier est lui toujours actif. La commande de la ligne 29, tapée au clavier, l'arrête.

Protocole SMTP

Cacher Sélectionnez
  1. ...\Chap9\04\bin\Release>ClientTcpGenerique smtp.neuf.fr 25
  2. Tapez vos commandes (bye pour arrêter) :
  3. <-- 220 neuf-infra-smtp-out-sp604002av.neufgp.fr neuf telecom Service relais mail ready
  4. HELO istia.univ-angers.fr
  5. <-- 250 neuf-infra-smtp-out-sp604002av.neufgp.fr hello [84.100.189.193], Banniere OK , pret pour envoyer un mail
  6. mail from: xx@neuf.fr
  7. <-- 250 2.1.0 <xx@neuf.fr> sender ok
  8. rcpt to: yy@univ-angers.fr
  9. <-- 250 2.1.5 <yy@univ-angers.fr> destinataire ok
  10. data
  11. <-- 354 enter mail, end with "." on a line by itself
  12. ligne1
  13. ligne2
  14. .
  15. <-- 250 2.0.0 M0jL1Z0044AoCxw0200000 message ok
  16. quit
  17. <-- 221 2.0.0 neuf-infra-smtp-out-sp604002av.neufgp.fr neuf telecom closing connection
  18. [Fin du thread de lecture des réponses du serveur]
  19. bye
  20.
  21. ...\Chap9\04\bin\Release>

Le lecteur est invité à relire les explications données au paragraphe , page et à tester les autres exemples utilisés avec le client putty .

XII-F-4. Un serveur Tcp générique

Maintenant nous nous intéressons à un serveur

  • qui affiche à l'écran les commandes envoyées par ses clients
  • leur envoie comme réponse les lignes de texte tapées au clavier par un utilisateur. C'est donc ce dernier qui fait office de serveur.

Le programme est lancé dans une fenêtre Dos par : ServeurTcpGenerique portEcoute, où portEcoute est le port sur lequel les clients doivent se connecter. Le service au client sera assuré par deux threads :

  • le thread principal qui :
  • traitera les clients les uns après les autres et non en parallèle.
  • qui lira les lignes tapées au clavier par l'utilisateur et les enverra au client. L'utilisateur signalera par la commande bye qu'il clôt la connexion avec le client. C'est parce que la console ne peut être utilisée pour deux clients simultanément que notre serveur ne traite qu'un client à la fois.
  • un thread secondaire se consacrant exclusivement à la lecture des lignes de texte envoyées par le client

Le serveur lui ne s'arrête jamais sauf par un Ctrl-C tapé au clavier par l'utilisateur.

Voyons quelques exemples. Le serveur est lancé sur le port 100 et on utilise le client générique du paragraphe, page , pour lui parler. La fenêtre du client est la suivante :

Cacher Sélectionnez
  1. ...\Chap9\04\bin\Release>ClientTcpGenerique localhost 100
  2. Tapez vos commandes (bye pour arrêter) :
  3. commande 1 du client 1
  4. <-- réponse 1 au client 1
  5. commande 2 du client 1
  6. <-- réponse 2 au client 1
  7. bye

Les lignes commençant par <-- sont celles envoyées du serveur au client, les autres celles du client vers le serveur. La fenêtre du serveur est la suivante :

Cacher Sélectionnez
  1. ...\Chap9\05\bin\Release>ServeurTcpGenerique 100
  2. Serveur générique lancé sur le port 0.0.0.0:100
  3. Client 127.0.0.1:4165
  4. Tapez vos commandes (bye pour arrêter) :
  5. <-- commande 1 du client 1
  6. réponse 1 au client 1
  7. <-- commande 2 du client 1
  8. réponse 2 au client 1
  9. [Fin du thread de lecture des demandes du client]
  10. bye

Les lignes commençant par <-- sont celles envoyées du client au serveur, les autres celles envoyées par le serveur au client. La ligne 9 indique que le thread de lecture des demandes du client s'est arrêté. Le thread principal du serveur est toujours en attente de commandes tapées au clavier pour les envoyer au client. Il faut alors taper au clavier la commande bye de la ligne 10 pour passer au client suivant. Le serveur est encore actif alors que le client 1 est terminé. On lance un second client pour le même serveur :

Cacher Sélectionnez
  1. ...\Chap9\04\bin\Release>ClientTcpGenerique localhost 100
  2. Tapez vos commandes (bye pour arrêter) :
  3. commande 3 du client 2
  4. <-- réponse 3 au client 2
  5. bye

La fenêtre du serveur est alors celle-ci :

Cacher Sélectionnez
  1. Tapez vos commandes (bye pour arrêter) :
  2. Client 127.0.0.1:4166
  3. <-- commande 3 du client 2
  4. réponse 3 au client 2
  5. [Fin du thread de lecture des demandes du client]
  6. bye

Après la ligne 6 ci-dessus, le serveur est passé en attente d'un nouveau client. On peut l'arrêter par Ctrl-C.

Simulons maintenant un serveur web en lançant notre serveur générique sur le port 88 :

Cacher Sélectionnez
  1. ...\Chap9\05\bin\Release>ServeurTcpGenerique 88
  2.
  3. Serveur générique lancé sur le port 0.0.0.0:88

Prenons maintenant un navigateur et demandons l'URL http://localhost:88/exemple.html . Le navigateur va alors se connecter sur le port 88 de la machine localhost puis demander la page /exemple.html :

Image non disponible

Regardons maintenant la fenêtre de notre serveur :

Cacher Sélectionnez
  1. Serveur générique lancé sur le port 0.0.0.0:88
  2. Client 127.0.0.1:4167
  3. Tapez vos commandes (bye pour arrêter) :
  4. <-- GET /exemple.html HTTP/1.1
  5. <-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/msword, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-appl
  6. ication, application/x-silverlight, */*
  7. <-- Accept-Language: fr,en-US;q=0.7,fr-FR;q=0.3
  8. <-- UA-CPU: x86
  9. <-- Accept-Encoding: gzip, deflate
  10. <-- User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.
  11. 4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.590; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
  12. <-- Host: localhost:88
  13. <-- Connection: Keep-Alive
  14. <--

On découvre les entêtes HTTP envoyés par le navigateur. Cela nous permet de découvrir d'autres entêtes HTTP que ceux déjà rencontrés. Elaborons une réponse à notre client. L'utilisateur au clavier est ici le véritable serveur et il peut élaborer une réponse à la main. Rappelons-nous la réponse faite par un serveur Web dans un précédent exemple :

Cacher Sélectionnez
  1. HTTP/1.1 200 OK
  2. Date: Sat, 03 May 2008 07:53:47 GMT
  3. Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
  4. X-Powered-By: PHP/4.4.4-8+etch4
  5. Set-Cookie: fe_typo_user=0d2e64b317; path=/
  6. Connection: close
  7. Transfer-Encoding: chunked
  8. Content-Type: text/html;charset=iso-8859-1
  9.
  10. 693f
  11. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"                                                                        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  12.          <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr_FR" lang="fr_FR">
  13. ....
  14.          </html>
  15. 0

Essayons de donner une réponse analogue en s'en tenant au strict mimimum :

Cacher Sélectionnez
  1. HTTP/1.1 200 OK
  2. Server: serveur tcp generique
  3. Connection: close
  4. Content-Type: text/html
  5.
  6. <html>
  7. <head><title>Serveur generique</title></head>
  8. <body><h2>Reponse du serveur generique</h2></body>
  9. </html>
  10. bye
  11. Flux de lecture des lignes de texte du client : l'erreur suivante s'est produite : Unable to read data from the transport connection: Une opération de blocage a été interrompue par un appel à WSACancelBlockingCall.
  12. [Fin du thread de lecture des demandes du client]

Nous nous sommes limités dans notre réponse aux entêtes HTTP des lignes 1-4. Nous ne donnons pas la taille du document que nous allons envoyer ( Content-Length ) mais nous contentons de dire que nous allons fermer la connexion ( Connection: close ) après envoi de celui-ci. Cela est suffisant pour le navigateur. En voyant la connexion fermée, il saura que la réponse du serveur est terminée et affichera la page HTML qui lui a été envoyée. Cette dernière est celle des lignes 6-9. L'utilisateur au clavier ferme ensuite la connexion au client en tapant la commande bye , ligne 10. Sur cette commande clavier le thread principal ferme la connexion avec le client. Ceci provoque l'exception de la ligne 11. Le thread de lecture des lignes de texte du client a été interrompu brutalement par la fermeture de la liaison avec le client et a lancé une exception. Après la ligne 12, le serveur se met en attente d'un nouveau client.

Le navigateur client affiche désormais la chose suivante :

Image non disponible

Si ci-dessus, on fait Affichage/Source pour voir ce qu'a reçu le navigateur, on obtient [2], c'est-à-dire exactement ce qu'on a envoyé depuis le serveur générique.

Le code du serveur TCP générique est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.IO;
  3. using System.Net;
  4. using System.Net.Sockets;
  5. using System.Threading;
  6.
  7. namespace Chap9 {
  8.     public class ServeurTcpGenerique {
  9.         public const string syntaxe = "Syntaxe : ServeurGénérique Port";
  10.
  11.         // programme principal
  12.         public static void Main(string[] args) {
  13.
  14.             // y-a-t-il un argument ?
  15.             if (args.Length != 1) {
  16.                 Console.WriteLine(syntaxe);
  17.                 Environment.Exit(1);
  18.             }
  19.             // cet argument doit être entier >0
  20.             int port = 0;
  21.             if (!int.TryParse(args[0], out port) || port <= 0) {
  22.                 Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine);
  23.                 Environment.Exit(2);
  24.             }
  25.             // on crée le service d'écoute
  26.             TcpListener ecoute = null;
  27.             try {
  28.                 // on crée le service
  29.                 ecoute = new TcpListener(IPAddress.Any, port);
  30.                 // on le lance
  31.                 ecoute.Start();
  32.                 // suivi
  33.                 Console.WriteLine("Serveur générique lancé sur le port {0}", ecoute.LocalEndpoint);
  34.                 while (true) {
  35.                     // attente d'un client
  36.                     Console.WriteLine("Attente du client suivant...");
  37.                     TcpClient tcpClient = ecoute.AcceptTcpClient();
  38.                     Console.WriteLine("Client {0}", tcpClient.Client.RemoteEndPoint);
  39.                     // on lance un thread à part pour lire les lignes de texte envoyées par le client
  40.                     ThreadPool.QueueUserWorkItem(Receive, tcpClient);
  41.                     // la lecture des commandes clavier se fait dans le thread principal
  42.                     Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
  43.                     string réponse = null;        // réponse serveur
  44.                     // on exploite la connexion client
  45.                     using (tcpClient) {
  46.                         // on crée un flux d'écriture vers le client
  47.                         using (NetworkStream networkStream = tcpClient.GetStream()) {
  48.                             using (StreamWriter writer = new StreamWriter(networkStream)) {
  49.                                 // flux de sortie non bufferisé
  50.                                 writer.AutoFlush = true;
  51.                                 // boucle de saisie des réponses au clavier
  52.                                 while (true) {
  53.                                     réponse = Console.ReadLine();
  54.                                     // fini ?
  55.                                     if (réponse.Trim().ToLower() == "bye")
  56.                                         break;
  57.                                     // on envoie la demande au client
  58.                                     writer.WriteLine(réponse);
  59.                                 }
  60.                             }
  61.                         }
  62.                     }
  63.                 }
  64.             } catch (Exception ex) {
  65.                 // on signale l'erreur
  66.                 Console.WriteLine("Main : l'erreur suivante s'est produite : {0}", ex.Message);
  67.             } finally {
  68.                 // fin de l'écoute
  69.                 ecoute.Stop();
  70.             }
  71.         }
  72.
  73.         // thread de lecture serveur <-- client
  74.         public static void Receive(object infos) {
  75.             // données locales
  76.             string demande = null;    // demande du client
  77.             string idClient=null;    // identité du client
  78.
  79.             // exploitation connexion client
  80.             try {
  81.                 using (TcpClient tcpClient = infos as TcpClient) {
  82.                     // identité client
  83.                     idClient = tcpClient.Client.RemoteEndPoint.ToString();
  84.                     using (NetworkStream networkStream = tcpClient.GetStream()) {
  85.                         using (StreamReader reader = new StreamReader(networkStream)) {
  86.                             // boucle lecture en continu des lignes de texte du flux d'entrée
  87.                             while ((demande = reader.ReadLine()) != null) {
  88.                                 // affichage console
  89.                                 Console.WriteLine("<-- {0}", demande);
  90.                             }
  91.                         }
  92.                     }
  93.                 }
  94.             } catch (Exception ex) {
  95.                 // erreur
  96.                 Console.WriteLine("Flux de lecture des lignes de texte du client {1} : l'erreur suivante s'est produite : {0}", ex.Message,idClient);
  97.             } finally {
  98.                 // on signale la fin du thread de lecture
  99.                 Console.WriteLine("Fin du thread de lecture des lignes de texte du client {0}. Si besoin est, arrêtez le thread de lecture console du serveur pour ce client, avec la commande bye.", idClient);
  100.             }
  101.         }
  102.     }
  103. }
  • ligne 29 : le service d'écoute est créé mais pas démarré. Il écoute toutes les interfaces réseau de la machine.
  • ligne 31 : le service d'écoute est démarré
  • ligne 34 : boucle infini d'attente des clients. L'utilisateur arrêtera le serveur par Ctrl-C.
  • ligne 37 : attente d'un client - opération bloquante. Lorsque le client arrive, l'instance TcpClient rendue par la méthode AcceptTcpClient représente le côté serveur d'une connexion ouverte avec le client.
  • ligne 40 : le flux de lecture des demandes du client est confié à un thread à part.
  • ligne 45 : utilisation de la connexion au client dans une clause using afin d'être sûr qu'elle sera fermée quoiqu'il arrive.
  • ligne 47 : utilisation du flux réseau dans une clause using
  • ligne 48 : création dans une clause using d'un flux d'écriture sur le flux réseau
  • ligne 50 : le flux d'écriture sera non bufferisé
  • lignes 52-59 : boucle de saisie au clavier des commandes à envoyer au client
  • ligne 69 : fin du service d'écoute. Cette instruction ne sera jamais exécutée ici puisque le serveur est arrêté par Ctrl-C.
  • ligne 78 : la méthode Receive qui affiche en continu sur la console les lignes de texte envoyées par le client. On retrouve là ce qui a été vu pour le client TCP générique.

XII-F-5. Un client Web

Nous avons vu dans l'exemple précédent, certains des entêtes HTTP qu'envoyait un navigateur :

Cacher Sélectionnez
  1. <-- GET /exemple.html HTTP/1.1
  2. <-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/msword, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-appl
  3. ication, application/x-silverlight, */*
  4. <-- Accept-Language: fr,en-US;q=0.7,fr-FR;q=0.3
  5. <-- UA-CPU: x86
  6. <-- Accept-Encoding: gzip, deflate
  7. <-- User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.
  8. 4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.590; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
  9. <-- Host: localhost:88
  10. <-- Connection: Keep-Alive
  11. <--

Nous allons écrire un client Web auquel on passerait en paramètre une URL et qui afficherait à l'écran le texte envoyé par le serveur. Nous supposerons que celui-ci supporte le protocole HTTP 1.1. Des entêtes précédents, nous n'utiliserons que les suivants :

Cacher Sélectionnez
  1. <-- GET /exemple.html HTTP/1.1
  2. <-- Host: localhost:88
  3. <-- Connection: close
  4. <--
  • le premier entête indique le document désiré
  • le second le serveur interrogé
  • le troisième que nous souhaitons que le serveur ferme la connexion après nous avoir répondu.

Si ci-dessus ligne 1, nous remplaçons GET par HEAD, le serveur ne nous enverra que les entêtes HTTP et pas le document précisé ligne 1.

Notre client web sera appelé de la façon suivante : ClientWeb URL cmd, où URL est l'URL désirée et cmd l'un des deux mots clés GET ou HEAD pour indiquer si on souhaite seulement les entêtes (HEAD) ou également le contenu de la page (GET). Regardons un premier exemple :

Cacher Sélectionnez
  1. ...\Chap9\06\bin\Release>ClientWeb http://istia.univ-angers.fr:80 HEAD
  2. HTTP/1.1 200 OK
  3. Date: Sat, 03 May 2008 14:05:24 GMT
  4. Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
  5. X-Powered-By: PHP/4.4.4-8+etch4
  6. Set-Cookie: fe_typo_user=e668408ac1; path=/
  7. Connection: close
  8. Content-Type: text/html;charset=iso-8859-1
  9.
  10. ...\Chap9\06\bin\Release>
  • ligne 1, nous ne demandons que les entêtes HTTP (HEAD)
  • lignes 2-9 : la réponse du serveur

Si nous utilisons GET au lieu de HEAD dans l'appel au client Web, nous obtenons le même résultat qu'avec HEAD avec de plus le corps du document demandé.

Le code du client web est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.IO;
  3. using System.Net.Sockets;
  4.
  5. namespace Chap9 {
  6.     class ClientWeb {
  7.         static void Main(string[] args) {
  8.             // syntaxe
  9.             const string syntaxe = "pg URI GET/HEAD";
  10.
  11.             // nombre d'arguments
  12.             if (args.Length != 2) {
  13.                 Console.WriteLine(syntaxe);
  14.                 return;
  15.             }
  16.
  17.             // on note l'URI demandée
  18.             string stringURI = args[0];
  19.             string commande = args[1].ToUpper();
  20.
  21.             // vérification validité de l'URI
  22.             if(! stringURI.StartsWith("http://")){
  23.                 Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
  24.                 return;
  25.             }
  26.             Uri uri = null;
  27.             try {
  28.                 uri = new Uri(stringURI);
  29.             } catch (Exception ex) {
  30.                 // URI incorrecte
  31.                 Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
  32.                 return;
  33.             }
  34.             // vérification de la commande
  35.             if (commande != "GET" && commande != "HEAD") {
  36.                 // commande incorrecte
  37.                 Console.WriteLine("Le second paramètre doit être GET ou HEAD");
  38.                 return;
  39.             }
  40.
  41.             try {
  42.                 // on se connecte au service
  43.                 using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) {
  44.                     using (NetworkStream networkStream = tcpClient.GetStream()) {
  45.                         using (StreamReader reader = new StreamReader(networkStream)) {
  46.                             using (StreamWriter writer = new StreamWriter(networkStream)) {
  47.                                 // flux de sortie non bufferisé
  48.                                 writer.AutoFlush = true;
  49.                                 // on demande l'URL - envoi des entêtes HTTP
  50.                                 writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
  51.                                 writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
  52.                                 writer.WriteLine("Connection: close");
  53.                                 writer.WriteLine();
  54.                                 // on lit la réponse
  55.                                 string réponse = null;
  56.                                 while ((réponse = reader.ReadLine()) != null) {
  57.                                     // on affiche la réponse sur la console
  58.                                     Console.WriteLine(réponse);
  59.                                 }
  60.                             }
  61.                         }
  62.                     }
  63.                 }
  64.             } catch (Exception e) {
  65.                 // on affiche l'exception
  66.                 Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
  67.             }
  68.         }
  69.     }
  70. }

La seule nouveauté dans ce programme est l'utilisation de la classe Uri. Le programme reçoit une URL ( Uniform Resource Locator ) ou URI ( Uniform Resource Identifier ) de la forme http://serveur:port/cheminPageHTML?param1=val1;param2=val2;… . La classe Uri nous permet de décomposer la chaîne de l'URL en ses différents éléments.

  • lignes 26-33 : un objet Uri est construit à partir de la chaîne stringURI reçue en paramètre. Si la chaîne URI reçue en paramètre n'est pas une URI valide (absence du protocole, du serveur…), une exception est lancée. Cela nous permet de vérifier la validité du paramètre reçu. Une fois l'objet Uri construit, on a accès aux différents éléments de cette Uri. Ainsi si l'objet uri du code précédent a été construit à partir de la chaîne http://serveur:port/document?param1=val1&param2=val2;… on aura :
  • uri.Host= serveur ,
  • uri.Port= port ,
  • uri.Path= document ,
  • uri.Query= param1=val1&param2=val2;… ,
  • uri.pathAndQuery= cheminPageHTML?param1=val1&param2=val2;… ,
  • uri.Scheme =http .

XII-F-6. Un client Web gérant les redirections

Le client Web précédent ne gère pas une éventuelle redirection de l'URL qu'il a demandée. Voici un exemple :

Cacher Sélectionnez
  1. ...\Chap9\06\bin\Release>ClientWeb http://www.ibm.com GET
  2. HTTP/1.1 302 Found
  3. Date: Sat, 03 May 2008 14:50:52 GMT
  4. Server: IBM_HTTP_Server
  5. Location: http://www.ibm.com/us/
  6. Content-Length: 206
  7. Kp-eeAlive: timeout=10, max=73
  8. Connection: Keep-Alive
  9. Content-Type: text/html
  10.
  11. <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
  12. <html><head>
  13. <title>302 Found</title>
  14. </head><body>
  15. <h1>Found</h1>
  16. <p>The document has moved <a href="http://www.ibm.com/us/">here</a>.</p>
  17. </body></html>
  • ligne 2 : le code 302 Found indique une redirection. L'adresse vers laquelle le navigateur doit se rediriger est dans le corps du document, ligne 16.

Un deuxième exemple :

Cacher Sélectionnez
  1. ...\Chap9\06\bin\Release>ClientWeb http://www.bull.com GET
  2. HTTP/1.1 301 Moved Permanently
  3. Date: Sat, 03 May 2008 14:52:31 GMT
  4. Server: Apache/1.3.33 (Unix) WS_filter/2.1.15 PHP/4.3.4
  5. X-Powered-By: PHP/4.3.4
  6. Location: http://www.bull.com/index.php
  7. Connection: close
  8. Transfer-Encoding: chunked
  9. Content-Type: text/html
  10.
  11. 0
  • ligne 2 : le code 301 Moved Permanently indique une redirection. L'adresse vers laquelle le navigateur doit se rediriger est indiquée ligne 6, dans l'entête HTTP Location.

Un troisième exemple :

Cacher Sélectionnez
  1. ...\Chap9\06\bin\Release>ClientWeb http://www.gouv.fr GET
  2. HTTP/1.1 302 Moved Temporarily
  3. Server: AkamaiGHost
  4. Content-Length: 0
  5. Location: http://www.premier-ministre.gouv.fr/fr/
  6. Date: Sat, 03 May 2008 14:56:53 GMT
  7. Connection: close
  • ligne 2 : le code 302 Moved Temporarily indique une redirection. L'adresse vers laquelle le navigateur doit se rediriger est indiquée ligne 5, dans l'entête HTTP Location.

Un quatrième exemple avec un serveur IIS local à la machine :

Cacher Sélectionnez
  1. ...\istia\Chap9\06\bin\Release>ClientWeb.exe http://localhost HEAD
  2. HTTP/1.1 302 Object moved
  3. Server: Microsoft-IIS/5.1
  4. Date: Sun, 04 May 2008 10:16:56 GMT
  5. Connection: close
  6. Location: localstart.asp
  7. Content-Length: 121
  8. Content-Type: text/html
  9. Set-Cookie: ASPSESSIONIDQQASDQAB=FDJLADLCOLDHGKGNIPMLHIIA; path=/
  10. Cache-control: private
  • ligne 2 : le code 302 Object moved indique une redirection. L'adresse vers laquelle le navigateur doit se rediriger est indiquée ligne 5, dans l'entête HTTP Location. On notera que contrairement aux exemples précédents, l'adresse de redirection est relative. L'adresse complète est en fait http://localhost/localstart.asp .

Nous nous proposons de gérer les redirections lorsque la première ligne des entêtes HTTP contient le mot clé moved (insensible à la casse) et que l'adresse de redirection est dans l'entête HTTP Location.

Si nous reprenons les trois derniers exemples, nous avons les résultats suivants :

Url : http://www.bull.com

Cacher Sélectionnez
  1. ...\Chap9\06B\bin\Release>ClientWebAvecRedirection http://www.bull.com HEAD
  2. HTTP/1.1 301 Moved Permanently
  3. Date: Sun, 04 May 2008 10:22:48 GMT
  4. Server: Apache/1.3.33 (Unix) WS_filter/2.1.15 PHP/4.3.4
  5. X-Powered-By: PHP/4.3.4
  6. Location: http://www.bull.com/index.php
  7. Connection: close
  8. Content-Type: text/html
  9.
  10.
  11. <--Redirection vers l'URL http://www.bull.com/index.php-->
  12.
  13. HTTP/1.1 200 OK
  14. Date: Sun, 04 May 2008 10:22:49 GMT
  15. Server: Apache/1.3.33 (Unix) WS_filter/2.1.15 PHP/4.3.4
  16. X-Powered-By: PHP/4.3.4
  17. Connection: close
  18. Content-Type: text/html
  • ligne 11 : la redirection a lieu vers l'adresse de la ligne 6

Url : http://www.gouv.fr

Cacher Sélectionnez
  1. ...\Chap9\06B\bin\Release>ClientWebAvecRedirect
  2. ion http://www.gouv.fr HEAD
  3. HTTP/1.1 302 Moved Temporarily
  4. Server: AkamaiGHost
  5. Content-Length: 0
  6. Location: http://www.premier-ministre.gouv.fr/fr/
  7. Date: Sun, 04 May 2008 10:30:38 GMT
  8. Connection: close
  9.
  10.
  11. <--Redirection vers l'URL http://www.premier-ministre.gouv.fr/fr/-->
  12.
  13. HTTP/1.1 200 OK
  14. Server: Apache
  15. X-Powered-By: PHP/4.4.1
  16. Last-Modified: Sun, 04 May 2008 10:29:48 GMT
  17. Content-Type: text/html
  18. Expires: Sun, 04 May 2008 10:40:38 GMT
  19. Date: Sun, 04 May 2008 10:30:38 GMT
  20. Connection: close
  • ligne 11 : la redirection a lieu vers l'adresse de la ligne 6

Url : http://localhost

Cacher Sélectionnez
  1. ...\Chap9\06B\bin\Release>ClientWebAvecRedirection.exe http://localhost HEAD
  2. HTTP/1.1 302 Object moved
  3. Server: Microsoft-IIS/5.1
  4. Date: Sun, 04 May 2008 10:37:11 GMT
  5. Connection: close
  6. Location: localstart.asp
  7. Content-Length: 121
  8. Content-Type: text/html
  9. Set-Cookie: ASPSESSIONIDQQASDQAB=GDJLADLCJCMPCHFFEJEFPKMK; path=/
  10. Cache-control: private
  11.
  12.
  13. <--Redirection vers l'URL http://localhost/localstart.asp-->
  14.
  15. HTTP/1.1 401 Access Denied
  16. Server: Microsoft-IIS/5.1
  17. Date: Sun, 04 May 2008 10:37:11 GMT
  18. WWW-Authenticate: Negotiate
  19. WWW-Authenticate: NTLM
  20. WWW-Authenticate: Basic realm="localhost"
  21. Connection: close
  22. Content-Length: 4766
  23. Content-Type: text/html
  • ligne 13 : la redirection a lieu vers l'adresse de la ligne 6
  • ligne 15 : l'accès à la page http://localhost/localstart.asp nous a été refusé.

Le programme gérant la redirection est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.IO;
  3. using System.Net.Sockets;
  4. using System.Text.RegularExpressions;
  5.
  6. namespace Chap9 {
  7.     class ClientWebAvecRedirection {
  8.         static void Main(string[] args) {
  9.             // syntaxe
  10.             const string syntaxe = "pg URI GET/HEAD";
  11.
  12.             // nombre d'arguments
  13.             if (args.Length != 2) {
  14.                 Console.WriteLine(syntaxe);
  15.                 return;
  16.             }
  17.
  18.             // on note l'URI demandée
  19.             string stringURI = args[0];
  20.             string commande = args[1].ToUpper();
  21.
  22.             // vérification validité de l'URI
  23.             if (!stringURI.StartsWith("http://")) {
  24.                 Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
  25.                 return;
  26.             }
  27.             Uri uri = null;
  28.             try {
  29.                 uri = new Uri(stringURI);
  30.             } catch (Exception ex) {
  31.                 // URI incorrecte
  32.                 Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
  33.                 return;
  34.             }
  35.             // vérification de la commande
  36.             if (commande != "GET" && commande != "HEAD") {
  37.                 // commande incorrecte
  38.                 Console.WriteLine("Le second paramètre doit être GET ou HEAD");
  39.                 return;
  40.             }
  41.
  42.             const int nbRedirsMax = 1;        // pas plus d'une redirection acceptée
  43.             int nbRedirs = 0;                            // nombre de redirections en cours
  44.
  45.             // expression régulière pour trouver une URL de redirection
  46.             Regex location = new Regex(@"^Location: (.+?)$");
  47.             try {
  48.                 // on peut avoir plusieurs URL à demander s'il y a des redirections
  49.                 while (nbRedirs <= nbRedirsMax) {
  50.                     // gestion redirection
  51.                     bool redir = false;
  52.                     bool locationFound = false;
  53.                     string locationString = null;
  54.                     // on se connecte au service
  55.                     using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) {
  56.                         using (StreamReader reader = new StreamReader(tcpClient.GetStream())) {
  57.                             using (StreamWriter writer = new StreamWriter(tcpClient.GetStream())) {
  58.                                 // flux de sortie non bufferisé
  59.                                 writer.AutoFlush = true;
  60.                                 // on demande l'URL - envoi des entêtes HTTP
  61.                                 writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
  62.                                 writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
  63.                                 writer.WriteLine("Connection: close");
  64.                                 writer.WriteLine();
  65.                                 // on lit la première ligne de la réponse
  66.                                 string premièreLigne = reader.ReadLine();
  67.                                 // écho écran
  68.                                 Console.WriteLine(premièreLigne);
  69.
  70.                                 // redirection ?
  71.                                 if (Regex.IsMatch(premièreLigne.ToLower(), @"\s+moved\s*")) {
  72.                                     // il y a une redirection
  73.                                     redir = true;
  74.                                     nbRedirs++;
  75.                                 }
  76.
  77.                                 // entêtes HTTP suivants jusqu'à trouver la ligne vide signalant la fin des entêtes
  78.                                 string réponse = null;
  79.                                 while ((réponse = reader.ReadLine()) != "") {
  80.                                     // on affiche la réponse
  81.                                     Console.WriteLine(réponse);
  82.                                     // s'il y a redirection, on recherche l'entête Location
  83.                                     if (redir && !locationFound) {
  84.                                         // on compare la ligne courante à l'expression relationnelle location
  85.                                         Match résultat = location.Match(réponse);
  86.                                         if (résultat.Success) {
  87.                                             // si on a trouvé, on note l'URL de redirection
  88.                                             locationString = résultat.Groups[1].Value;
  89.                                             // on note qu'on a trouvé
  90.                                             locationFound = true;
  91.                                         }
  92.                                     }
  93.                                 }
  94.
  95.                                 // les entêtes HTTP ont été épuisés - on écrit la ligne vide
  96.                                 Console.WriteLine(réponse);
  97.                                 // puis on passe au corps du document
  98.                                 while ((réponse = reader.ReadLine()) != null) {
  99.                                     Console.WriteLine(réponse);
  100.                                 }
  101.                             }
  102.                         }
  103.                     }
  104.                     // a-t-on fini ?
  105.                     if (!locationFound || nbRedirs > nbRedirsMax)
  106.                         break;
  107.                     // il y a une redirection à opérer - on construit la nouvelle Uri
  108.                     try {
  109.                         if (locationString.StartsWith("http")) {
  110.                             // adresse http complète
  111.                             uri = new Uri(locationString);
  112.                         } else {
  113.                             // adresse http relative à l'uri courante
  114.                             uri = new Uri(uri, locationString);
  115.                         }
  116.                         // log console
  117.                         Console.WriteLine("\n<--Redirection vers l'URL {0}-->\n", uri);
  118.                     } catch (Exception ex) {
  119.                         // pb avec l'Uri
  120.                         Console.WriteLine("\n<--L'adresse de redirection {0} n'a pas été comprise : {1} -->\n", locationString, ex.Message);
  121.                     }
  122.                 }
  123.             } catch (Exception e) {
  124.                 // on affiche l'exception
  125.                 Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
  126.             }
  127.         }
  128.     }
  129. }

Par rapport à la version précédente, les changements sont les suivants :

  • ligne 46 : l'expression régulière pour récupérer l'adresse de redirection dans l'entête HTTP Location: adresse .
  • ligne 49 : le code qui était exécuté précédemment pour une unique Uri peut l'être maintenant successivement pour plusieurs Uri.
  • ligne 66 : on lit la 1re ligne des entêtes HTTP envoyés par le serveur. C'est elle qui contient le mot clé moved si le document demandé a été déplacé.
  • lignes 71-75 : on vérifie si la 1re ligne contient le mot clé moved . Si oui, on le note.
  • lignes 79-93 : lecture des autres entêtes HTTP jusqu'à rencontrer la ligne vide qui signale leur fin. Si la 1re ligne annonçait une redirection, on s'attarde alors sur l'entête HTTP Location: adresse pour mémoriser l'adresse de redirection dans locationString .
  • lignes 98-100 : le reste de la réponse du serveur HTTP est affiché à la console.
  • lignes 105-106 : l'Uri demandée a été entièrement exploitée et affichée. S'il n'y a pas de redirection à faire ou si le nombre de redirections autorisées est dépassé, on quitte le programme.
  • lignes 108-122 : s'il y a redirection, on calcule la nouvelle Uri à demander. Il y a une petite gymnastique à faire selon que l'adresse de redirection trouvée était absolue (ligne 111) ou relative (ligne 114).

XII-G. Les classes .NET spécialisées dans un protocole particulier de l'internet

Dans les exemples précédents du client web, le protocole HTTP était géré avec un client TCP. Il nous fallait donc gérer nous-mêmes le protocole de communication particulier utilisé. Nous aurions pu construire de façon analogue, un client SMTP ou POP. Le framework .NET offre des classes spécialisées pour les protocoles HTTP et SMTP. Ces classes connaissent le protocole de communication entre le client et le serveur et évitent au développeur d'avoir à les gérer. Nous les présentons maintenant.

XII-G-1. La classeWebClient

Il existe une classe WebClient sachant dialoguer avec un serveur web. Considérons l'exemple du client web du paragraphe , page , traité ici avec la classe WebClient .

Cacher Sélectionnez
  1. using System;
  2. using System.IO;
  3. using System.Net;
  4. namespace Chap9 {
  5.     public class Program {
  6.         public static void Main(string[] args) {
  7.             // syntaxe : [prog] Uri
  8.             const string syntaxe = "pg URI";
  9.
  10.             // nombre d'arguments
  11.             if (args.Length != 1) {
  12.                 Console.WriteLine(syntaxe);
  13.                 return;
  14.             }
  15.
  16.             // on note l'URI demandée
  17.             string stringURI = args[0];
  18.
  19.             // vérification validité de l'URI
  20.             if (!stringURI.StartsWith("http://")) {
  21.                 Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
  22.                 return;
  23.             }
  24.             Uri uri = null;
  25.             try {
  26.                 uri = new Uri(stringURI);
  27.             } catch (Exception ex) {
  28.                 // URI incorrecte
  29.                 Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
  30.                 return;
  31.             }
  32.
  33.             try {
  34.                 // création client web
  35.                 using (WebClient client = new WebClient()) {
  36.                     // ajout d'un entête HTTP
  37.                     client.Headers.Add("user-agent", "st");
  38.                     using (Stream stream = client.OpenRead(uri)) {
  39.                         using (StreamReader reader = new StreamReader(stream)) {
  40.                             // affichage réponse du serveur web
  41.                             Console.WriteLine(reader.ReadToEnd());
  42.                             // affichage entêtes réponse du serveur
  43.                             Console.WriteLine("---------------------");
  44.                             foreach (string clé in client.ResponseHeaders.Keys) {
  45.                                 Console.WriteLine("{0}: {1}", clé, client.ResponseHeaders[clé]);
  46.                             }
  47.                             Console.WriteLine("---------------------");
  48.                         }
  49.                     }
  50.                 }
  51.             } catch (WebException e1) {
  52.                 Console.WriteLine("L'exception suivante s'est produite : {0}", e1);
  53.             } catch (Exception e2) {
  54.                 Console.WriteLine("L'exception suivante s'est produite : {0}", e2);
  55.             }
  56.         }
  57.     }
  58. }
  • ligne 35 : le client web est créé mais pas encore configuré
  • ligne 37 : on ajoute un entête HTTP à la demande HTTP qui va être faite. Nous allons découvrir que d'autres entêtes seront envoyés par défaut.
  • ligne 38 : le client web demande l'Uri donnée par l'utilisateur et lit le document envoyé. [WebClient].OpenRead(Uri) ouvre la connexion avec Uri et lit la réponse. C'est là l'intérêt de la classe. Elle s'occupe du dialogue avec le serveur web. Le résultat de la méthode OpenRead est de type Stream et représente le document demandé. Les entêtes HTTP envoyés par le serveur et qui précèdent le document dans la réponse n'en font pas partie.
  • ligne 39 : on utilise un StreamReader et ligne 41, sa méthode ReadToEnd pour lire la totalité de la réponse.
  • lignes 44-46 : on affiche les entêtes HTTP de la réponse du serveur. [WebClient].ResponseHeaders représente une collection valuée dont les clés sont les noms des entêtes HTTP et les valeurs, les chaînes de caractères associées à ces entêtes.
  • ligne 51 : les exceptions qui sont levées lors d'un échange client / serveur sont de type WebException .

Voyons quelques exemples.

On lance le serveur TCP générique construit au paragraphe , page :

Cacher Sélectionnez
  1. ...\Chap9\05\bin\Release>ServeurTcpGenerique.exe 88
  2. Serveur générique lancé sur le port 0.0.0.0:88

On lance le client web précédent de la façon suivante :

Cacher Sélectionnez
  1. ...\Chap9\09\bin\Release>09 http://localhost:88

L'Uri demandée est celle du serveur générique. Celui-ci affiche alors les entêtes HTTP que lui a envoyés le client web :

Cacher Sélectionnez
  1. Client 127.0.0.1:1415
  2. Tapez vos commandes (bye pour arrêter) :
  3. <-- GET / HTTP/1.1
  4. <-- User-Agent: st
  5. <-- Host: localhost:88
  6. <-- Connection: Keep-Alive
  7. <--

On voit ainsi :

  • que le client web envoie 3 entêtes HTTP par défaut (lignes 3, 5, 6)
  • ligne 4 : l'entête que nous avons généré nous-mêmes (ligne 37 du code)
  • que le client web utilise par défaut la méthode GET (ligne 3). Il existe d'autres méthodes parmi lesquelles POST et HEAD.

Maintenant demandons une ressource inexistante :

Cacher Sélectionnez
  1. ...\Chap9\09\bin\Release>09 http://istia.univ-angers.fr/inconnu
  2. L'exception suivante s'est produite : System.Net.WebException: The remote server returned an error: (404) Not Found.
  3.    at System.Net.WebClient.OpenRead(Uri address)
  4.    at System.Net.WebClient.OpenRead(String address)
  5.    at Chap9.WebClient1.Main(String[] args) in C:\data\2007-2008\c# 2008\poly\istia\Chap9\09\Program.cs:line 16
  • ligne 2 : on a eu une exception de type WebException parce que le serveur a répondu par le code 404 Not Found pour indiquer que la ressource demandée n'existait pas.

Enfin terminons en demandant une ressource existante :

Cacher Sélectionnez
  1. ...\istia\Chap9\09\bin\Release>09 http://istia.univ-angers.fr >istia.univ-angers.txt

Le fichier istia.univ-angers.txt produit par la commande est le suivant :

Cacher Sélectionnez
  1. <!DOCTYPE html
  2.      PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  3.      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  4. <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr_FR" lang="fr_FR">
  5. <head>
  6.     <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
  7. ...
  8. </html>
  9. ---------------------
  10. Keep-Alive: timeout=15, max=100
  11. Connection: Keep-Alive
  12. Transfer-Encoding: chunked
  13. Content-Type: text/html;charset=iso-8859-1
  14. Date: Sun, 04 May 2008 14:30:53 GMT
  15. Set-Cookie: fe_typo_user=22eaaf283a; path=/
  16. Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29
  17. X-Powered-By: PHP/4.4.4-8+etch4
  18. ---------------------
  • ligne 1 : le document HTML demandé.
  • lignes 3-10 : les entêtes de la réponse HTTP dans un ordre qui n'est pas forcément celui dans lequel ils ont été envoyés.

La classe WebClient dispose de méthodes permettant de recevoir un document (méthodes DownLoad ) ou d'en envoyer (méthodes UpLoad ) :

DownLoadData | pour télécharger une ressource en tant que tableau d'octets (image par exemple)
DownLoadFile | pour télécharger une ressource et la sauvegarder dans un fichier local
DownLoadString | pour télécharger une ressource et la récupérer en tant que chaîne de caractères (fichier html par exemple)
OpenWrite | le pendant d' OpenRead mais pour envoyer des données au serveur
UpLoadData | le pendant de DownLoadData mais vers le serveur
UpLoadFile | le pendant de DownLoadFile mais vers le serveur
UpLoadString | le pendant de DownLoadString mais vers le serveur
UpLoadValues | pour envoyer au serveur les données d'une commande POST et en récupérer les résultats sous la forme d'un tableau d'octets. La commande POST demande un document tout en transmettant au serveur des informations qui lui sont nécessaires pour déterminer le document réel à envoyer. Ces informations sont envoyées comme document au serveur, d'où le nom UpLoad de la méthode. Elles sont envoyées derrière la ligne vide des entêtes HTTP sous la forme param1=valeur1&param2=valeur2&… : POST /document HTTP/1.1 … [ligne vide] param1=valeur1&param2=valeur2&… Le même document pourrait être demandé avec la méthode GET : GET /document?param1=valeur1&param2=valeur2&… … [ligne vide] La différence entre les deux méthodes est que le navigateur affichant l'Uri demandée, affichera /document dans le cas du POST et /document?param1=valeur1&param2=valeur2&… dans le cas du GET.

XII-G-2. Les classes WebRequest / WebResponse

Parfois la classe WebClient n'est pas suffisamment souple pour faire ce que l'on souhaite. Reprenons l'exemple du client web avec redirection étudié au paragraphe , page . Il nous faut émettre l'entête HTTP :

Cacher Sélectionnez
  1. HEAD /document HTTP/1.1

Nous avons vu que les entêtes HTTP émis par défaut par le client web étaient les suivants :

Cacher Sélectionnez
  1. <-- GET / HTTP/1.1
  2. <-- Host: machine:port
  3. <-- Connection: Keep-Alive

Nous avons vu également qu'il était possible d'ajouter des entêtes HTTP aux précédents avec la collection [WebClient].Headers . Seulement la ligne 1 n'est pas un entête appartenant à la collection Headers car elle n'a pas la forme clé: valeur . Je n'ai pas trouvé comment changer le GET en HEAD dans la ligne 1 en partant de la classe WebClient (j'ai peut-être mal cherché ?). Lorsque la classe WebClient a atteint ses limites, on peut passer aux classes WebRequest / WebResponse :

  • WebRequest : représente la totalité de la demande du client Web.
  • WebResponse : représente la totalité de la réponse du serveur Web

Nous avons dit que la classe WebClient gérait les schémas http:, https:, ftp:, file:. Les requêtes et réponses de ces différents protocoles n'ont pas la même forme. Aussi est-il nécessaire de manipuler le type exact de ces éléments plutôt que leur type générique WebRequest et WebResponse . Aussi utilisera-t-on les classes :

  • HttpWebRequest, HttpWebResponse pour un client HTTP
  • FtpWebRequest, FtpWebResponse pour un client FTP

Nous traitons maintenant avec les classes HttpWebRequest et HttpWebresponse l'exemple du client web avec redirection étudié au paragraphe , page . Le code est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.IO;
  3. using System.Net.Sockets;
  4. using System.Net;
  5.
  6. namespace Chap9 {
  7.     class WebRequestResponse {
  8.         static void Main(string[] args) {
  9.             // syntaxe
  10.             const string syntaxe = "pg URI GET/HEAD";
  11.
  12.             // nombre d'arguments
  13.             if (args.Length != 2) {
  14.                 Console.WriteLine(syntaxe);
  15.                 return;
  16.             }
  17.
  18.             // on note l'URI demandée
  19.             string stringURI = args[0];
  20.             string commande = args[1].ToUpper();
  21.
  22.             // vérification validité de l'URI
  23.             Uri uri = null;
  24.             try {
  25.                 uri = new Uri(stringURI);
  26.             } catch (Exception ex) {
  27.                 // URI incorrecte
  28.                 Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
  29.                 return;
  30.             }
  31.             // vérification de la commande
  32.             if (commande != "GET" && commande != "HEAD") {
  33.                 // commande incorrecte
  34.                 Console.WriteLine("Le second paramètre doit être GET ou HEAD");
  35.                 return;
  36.             }
  37.
  38.             try {
  39.                 // on configure la requête
  40.                 HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
  41.                 httpWebRequest.Method = commande;
  42.                 httpWebRequest.Proxy = null;
  43.                 // on l'exécute
  44.                 HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse;
  45.                 // résultat
  46.                 Console.WriteLine("---------------------");
  47.                 Console.WriteLine("Le serveur {0} a répondu : {1} {2}", httpWebResponse.ResponseUri,(int)httpWebResponse.StatusCode, httpWebResponse.StatusDescription);
  48.                 // entêtes HTTP
  49.                 Console.WriteLine("---------------------");
  50.                 foreach (string clé in httpWebResponse.Headers.Keys) {
  51.                     Console.WriteLine("{0}: {1}", clé, httpWebResponse.Headers[clé]);
  52.                 }
  53.                 Console.WriteLine("---------------------");
  54.                 // document
  55.                 using (Stream stream = httpWebResponse.GetResponseStream()) {
  56.                     using (StreamReader reader = new StreamReader(stream)) {
  57.                         // on affiche la réponse sur la console
  58.                         Console.WriteLine(reader.ReadToEnd());
  59.                     }
  60.                 }
  61.             } catch (WebException e1) {
  62.                 // on récupère la réponse
  63.                 HttpWebResponse httpWebResponse = e1.Response as HttpWebResponse;
  64.                 Console.WriteLine("Le serveur {0} a répondu : {1} {2}", httpWebResponse.ResponseUri, (int)httpWebResponse.StatusCode, httpWebResponse.StatusDescription);
  65.             } catch (Exception e2) {
  66.                 // on affiche l'exception
  67.                 Console.WriteLine("L'erreur suivante s'est produite : {0}", e2.Message);
  68.             }
  69.         }
  70.     }
  71. }
  • ligne 40 : un objet de type WebRequest est créé avec la méthode statique WebRequest.Create(Uri uri) ou uri est l'uri du document à télécharger. Parce que l'on sait que le protocole de l'Uri est HTTP, le type du résultat est changé en HttpWebRequest afin d'avoir accès aux éléments spécifiques du protocole Http.
  • ligne 41 : nous fixons la méthode GET / POST / HEAD de la 1re ligne des entêtes HTTP. Ici ce sera GET ou HEAD.
  • ligne 42 : dans un réseau privé d'entreprise, il est fréquent que les machines de l'entreprise soit isolées de l'internet pour des raisons de sécurité. Pour cela, le réseau privé utilise des adresses internet que les routeurs de l'internet ne routent pas. Le réseau privé est relié à l'internet par des machines particulières appelées proxy qui sont reliées à la fois au réseau privé de l'entreprise et à l'internet. C'est un exemple de machines à plusieurs adresses IP. Une machine du réseau privé ne peut établir elle-même une connexion avec un serveur de l'internet, un serveur web par exemple. Elle doit demander à une machine proxy de le faire pour elle. Une machine proxy peut abriter des serveurs proxy pour différents protocoles. On parle de proxy HTTP pour désigner le service qui s'occupe de faire les requêtes HTTP pour le compte des machines du réseau privé. Si un tel serveur proxy HTTP existe, il faut l'indiquer dans le champ [WebRequest].proxy. On écrira par exemple :
Cacher Sélectionnez
  1. [WebRequest].proxy=new WebProxy("pproxy.istia.uang:3128");

si le proxy HTTP opère sur le port 3128 de la machine pproxy.istia.uang . On met null dans le champ [WebRequest].proxy si la machine a un accès direct à l'internet et n'a pas à passer par un proxy.

  • ligne 44 : la méthode GetResponse() demande le document identifié par son Uri et rend un objet WebRequestResponse qu'on transforme ici en objet HttpWebResponse . Cet objet représente la réponse du serveur à la demande du document.
  • ligne 47 :
  • [HttpWebResponse].ResponseUri : est l'Uri du serveur ayant envoyé le document. En cas de redirection, celle-ci peut être différente de l'Uri du serveur interrogé initialement. On notera que le code ne gère pas la redirection. Elle est gérée automatiquement par la méthode GetResponse . De nouveau, c'est l'avantage des classes de haut niveau vis à vis des classes basiques du protocole Tcp.
  • [HttpWebResponse].StatusCode , [HttpWebResponse].StatusDescription représentent la 1re ligne de la réponse, par exemple : HTTP/1.1 200 OK . StatusCode est 200 et StatusDescription est OK.
  • ligne 50 : [HttpWebResponse].Headers est la collection des entêtes HTTP de la réponse.
  • ligne 55 : [HttpWebResponse].GetResponseStream : est le flux qui permet d'obtenir le document contenu dans la réponse.
  • ligne 61 : il peut se produire une exception de type WebException
  • ligne 63 : [WebException].Response est la réponse qui a provoqué la levée de l'exception.

Voici un exemple d'exécution :

Cacher Sélectionnez
  1. ...\Chap9\09B\bin\Release>09B http://www.gouv.fr HEAD
  2. ---------------------
  3. Le serveur http://www.premier-ministre.gouv.fr/fr/ a répondu : 200 OK
  4. ---------------------
  5. Connection: keep-alive
  6. Content-Type: text/html; charset=iso-8859-1
  7. Date: Mon, 05 May 2008 13:02:29 GMT
  8. Expires: Mon, 05 May 2008 13:07:20 GMT
  9. Last-Modified: Mon, 05 May 2008 12:56:59 GMT
  10. Server: Apache
  11. X-Powered-By: PHP/4.4.1
  12. ---------------------
  • lignes 1 et 3 : le serveur qui a répondu n'est pas le même que celui qui avait été interrogé. Il y a donc eu redirection.
  • lignes 5-11 : les entêtes HTTP envoyés par le serveur

XII-G-3. Application : un client proxy d'un serveur web de traduction

Nous montrons maintenant comment les classes précédentes nous permettent d'exploiter les ressources du web.

XII-G-3-a. L'application

Il existe sur le web des sites de traduction. Celui qui sera utilisé ici est le site http://trans.voila.fr/traduction_voila.php :

Image non disponible

Le texte à traduire est inséré dans [1], le sens de traduction est choisi dans [2]. La traduction est demandée par [3] et obtenue en [4].

Nous allons écrire une application windows cliente de l'application ci-dessus. Elle ne fera rien de plus que l'application du site [trans.voila.fr]. Son interface sera la suivante :

Image non disponible

XII-G-3-b. L'architecture de l'application

L'application aura l'architecture 2 couches suivante :

Image non disponible

XII-G-3-c. Le projet Visual studio

Le projet Visual studio sera le suivant :

Image non disponible
  • en [1], la solution est composée de deux projets,
  • [2] : l'un pour la couche [dao] et les entités utilisées par celle-ci,
  • [3] : l'autre pour l'interface windows

XII-G-3-d. Le projet [dao]

Le projet [dao] est formé des éléments suivants :

  • IServiceTraduction.cs : l'interface présentée à la couche [ui]
  • ServiceTraduction : l'implémentation de cette interface
  • WebTraductionsException : une exception spécifique à l'application

L'interface IServiceTraduction est la suivante :

Cacher Sélectionnez
  1. using System.Collections.Generic;
  2.
  3. namespace dao {
  4.     public interface IServiceTraduction {
  5.         // langues utilisées
  6.         IDictionary<string, string> LanguesTraduites { get; }
  7.         // traduction
  8.         string Traduire(string texte, string deQuoiVersQuoi);
  9.     }
  10. }
  • ligne 6 : la propriété LanguesTraduites rend le dictionnaire des langues acceptées par le serveur de traduction. Ce dictionnaire a des entrées de la forme ["fe","Français-Anglais"] où la valeur désigne un sens de traduction, ici du Français vers l'Anglais, et la clé "fe" est un code utilisé par le serveur de traduction trans.voila.fr .
  • ligne 8 : la méthode Traduire est la méthode de traduction :
  • texte est le texte à traduire
  • deQuoiVersQuoi est l'une des clés du dictionnaire des langues traduites
  • la méthode rend la traduction du texte

ServiceTraduction est une classe d'implémentation de l'interface IServiceTraduction. Nous la détaillons dans la section qui suit.

WebTraductionsException est la classe d'exception suivante :

Cacher Sélectionnez
  1. using System;
  2.
  3. namespace entites {
  4.     public class WebTraductionsException : Exception {
  5.
  6.         // code d'erreur
  7.         public int Code { get; set; }
  8.
  9.         // constructeurs
  10.         public WebTraductionsException() {
  11.         }
  12.         public WebTraductionsException(string message)
  13.             : base(message) {
  14.         }
  15.         public WebTraductionsException(string message, Exception e)
  16.             : base(message, e) {
  17.         }
  18.     }
  19. }
  • ligne 7 : un code d'erreur

XII-G-3-e. Le client web [ServiceTraduction]

Revenons sur l'architecture de notre application :

Image non disponible

La classe [ServiceTraduction] que nous devons écrire est un client du service web de traduction [trans.voila.fr]. Pour l'écrire, il nous faut comprendre

  • ce qu'attend le serveur de traduction de son client
  • ce qu'il renvoie en retour à son client

Voyons sur un exemple le dialogue client / serveur qui intervient dans une traduction. Reprenons l'exemple présenté en introduction de l'application :

Image non disponible

Le texte à traduire est inséré dans [1], le sens de traduction est choisi dans [2]. La traduction est demandée par [3] et obtenue en [4].

Pour obtenir la traduction [4], le navigateur a envoyé la requête GET suivante (affichée dans son champ d'adresse) :

Cacher Sélectionnez
  1. http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection=fe&stext=ce+chien+est+malade

Elle est plutôt simple à comprendre :

  • http://trans.voila.fr/traduction_voila.php est l'Url du service de traduction
  • isText=1 semble vouloir dire qu'on a affaire à du texte
  • translationDirection désigne le sens de la traduction, ici Français-Anglais
  • stext est le texte à traduire sous une forme qu'on appelle Url encodée. En effet, certains caractères ne peuvent apparaître dans une Url. C'est le cas par exemple de l'espace qui a été ici encodé par un +. Le framework .Net offre la méthode statique System.Web.HttpUtility.UrlEncode pour faire ce travail d'encodage.

On en conclut que pour interroger le serveur de traduction, notre classe [ServiceTraduction] pourra utiliser la chaîne

Cacher Sélectionnez
  1. "http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}"

où les marqueurs {0} et {1} seront remplacés respectivement par le sens de traduction et le texte à traduire.

Comment connait-on les sens de traduction acceptés par le serveur ? Dans la copie d'écran ci-dessus, les langues traduites sont dans la liste déroulante. Si dans le navigateur on regarde (Affichage / source) le code Html de la page, on trouve ceci pour la liste déroulante :

Cacher Sélectionnez
  1. <select name="translationDirection" class="champs">
  2.     <option selected value='fe'>Fran&ccedil;ais vers Anglais
  3.     <option  value='ef'>Anglais vers Fran&ccedil;ais
  4.     <option  value='fg'>Fran&ccedil;ais vers Allemand
  5.     <option  value='gf'>Allemand vers Fran&ccedil;ais
  6.     <option  value='fs'>Fran&ccedil;ais vers Espagnol
  7.     <option  value='sf'>Espagnol vers Fran&ccedil;ais
  8.     <option  value='fr'>Fran&ccedil;ais vers Russe
  9.     <option  value='rf'>Russe vers Fran&ccedil;ais
  10.     <option  value='es'>Anglais vers Espagnol
  11.     <option  value='se'>Espagnol vers Anglais
  12.     <option  value='eg'>Anglais vers Allemand
  13.     <option  value='ge'>Allemand vers Anglais
  14.     <option  value='ep'>Anglais vers Portugais
  15.     <option  value='pe'>Portugais vers Anglais
  16.     <option  value='ie'>Italien vers Anglais
  17.     <option  value='gs'>Allemand vers Espagnol
  18.     <option  value='sg'>Espagnol vers Allemand
  19. </select>

Ce n'est pas un code Html très propre, dans la mesure où chaque balise <option> devrait être normalement fermée par une balise </option>. Ceci dit, les attributs value nous donnent la liste des codes de traduction qui doivent être envoyés au serveur. Dans le dictionnaire LanguesTraduites de l'interface IServiceTraduction, les clés seront les attributs value ci-dessus et les valeurs, les textes affichés par la liste déroulante.

Maintenant regardons (Affichage / source) où se trouve dans la page Html la traduction renvoyée par le serveur de traduction :

Cacher Sélectionnez
  1. ...
  2. <strong>Texte traduit : </strong><div class="txtTrad">this dog is sick</div>
  3. ...

La traduction se trouve au beau milieu de la page Html renvoyée. Comment la retrouver ? On peut utiliser une expression régulière avec la séquence <div class="txtTrad">…</div> car la balise <div class="txtTrad"> n'est présente qu'à cet endroit de la page Html. L'expression régulière C# permettant de récupérer le texte traduit est la suivante :

Cacher Sélectionnez
  1. @"<div class=""txtTrad"">(.*?)</div>"

Nous avons désormais les éléments pour écrire la classe d'implémentation ServiceTraduction de l'interface IServiceTraduction :

Cacher Sélectionnez
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Net;
  5. using System.Text.RegularExpressions;
  6. using System.Web;
  7. using entites;
  8.
  9. namespace dao {
  10.     public class ServiceTraduction : IServiceTraduction {
  11.         // propriétés automatiques de configuration du service
  12.         public IDictionary<string, string> LanguesTraduites { get; set; }
  13.         public string UrlServeurTraduction { get; set; }
  14.         public string ProxyHttp { get; set; }
  15.         public String RegexTraduction { get; set; }
  16.
  17.         // traduction
  18.         public string Traduire(string texte, string deQuoiVersQuoi) {
  19.             // la traduction demandée est-elle possible ?
  20.             if (!LanguesTraduites.ContainsKey(deQuoiVersQuoi)) {
  21.                 throw new WebTraductionsException(String.Format("Le sens de traduction [{0}] n'est pas reconnu")) { Code = 10 };
  22.             }
  23.             // texte à traduire
  24.             string texteATraduire = HttpUtility.UrlEncode(texte);
  25.             // uri à demander
  26.             string uri = string.Format(UrlServeurTraduction, deQuoiVersQuoi, texteATraduire);
  27.             // expression régulière pour retrouver la traduction dans la réponse
  28.             Regex patternTraduction = new Regex(RegexTraduction);
  29.             // exception
  30.             WebTraductionsException exception = null;
  31.             // traduction
  32.             string traduction = null;
  33.             try {
  34.                 // on configure la requête
  35.                 HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
  36.                 httpWebRequest.Method = "GET";
  37.                 httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ;
  38.                 // on l'exécute
  39.                 HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse;
  40.                 // document
  41.                 using (Stream stream = httpWebResponse.GetResponseStream()) {
  42.                     using (StreamReader reader = new StreamReader(stream)) {
  43.                         bool traductionTrouvée = false;
  44.                         string ligne = null;
  45.                         while (!traductionTrouvée && (ligne = reader.ReadLine()) != null) {
  46.                             // recherche traduction dans la ligne courante
  47.                             MatchCollection résultats = patternTraduction.Matches(ligne);
  48.                             // traduction trouvée ?
  49.                             if (résultats.Count != 0) {
  50.                                 traduction = résultats[0].Groups[1].Value.Trim();
  51.                                 traductionTrouvée = true;
  52.                             }
  53.                         }
  54.                         // traduction trouvée ?
  55.                         if (!traductionTrouvée) {
  56.                             exception = new WebTraductionsException("Le serveur n'a pas renvoyé de réponse") { Code = 12 };
  57.                         }
  58.                     }
  59.                 }
  60.             } catch (Exception e) {
  61.                 exception = new WebTraductionsException("Erreur rencontrée lors de la traduction", e) { Code = 11 };
  62.             }
  63.             // exception ?
  64.             if (exception != null) {
  65.                 throw exception;
  66.             } else {
  67.                 return traduction;
  68.             }
  69.         }
  70.     }
  71. }
  • ligne 12 : la propriété LanguesTraduites de l'interface IServiceTraduction - initialisée de l'extérieur
  • ligne 13 : la propriété UrlServeurTraduction est l'Url à demander au serveur de traduction : http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1} où le marqueur {0} devra être remplacé par le sens de traduction et le marqueur {1} par le texte à traduire - initialisée de l'extérieur
  • ligne 14 : la propriété ProxyHttp est l'éventuel proxy Http à utiliser, par exemple : pproxy.istia.uang:3128 - initialisée de l'extérieur
  • ligne 15 : la propriété RegexTraduction est l'expression régulière permettant de récupérer la traduction dans le flux Html renvoyé par le serveur de traduction, par exemple @"<div class=""txtTrad"">(.*?)</div>" - initialisée de l'extérieur
  • ces quatres propriétés seront, dans notre application, initialisées par Spring.
  • lignes 20-22 : on vérifie que le sens de traduction demandé existe bien dans le dictionnaire des langues traduites. Si ce n'est pas le cas, une exception est lancée.
  • ligne 24 : le texte à traduire est encodé pour pouvoir faire partie d'une Url
  • ligne 26 : l'Uri du service de traduction est construite. Si la propriété UrlServeurTraduction est la chaîne http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1} , le marqueur {0} est remplacé par le sens de traduction et le marqueur {1} par le texte à traduire.
  • ligne 28 : le modèle de recherche de la traduction dans la réponse html du serveur de traduction est construit.
  • lignes 33, 60 : l'opération d'interrogation du serveur de traduction se passe dans un try / catch
  • ligne 35 : l'objet HttpWebRequest qui va être utilisé pour interroger le serveur de traduction est construit avec l'Uri du document demandé.
  • ligne 36 : la méthode d'interrogation est GET. On pourrait se passer de cette instruction, car GET est probablement la méthode par défaut de l'objet HttpWebRequest .
  • ligne 37 : on fixe la propriété Proxy de l'objet HttpWebRequest .
  • ligne 39 : la requête au serveur de traduction est faite et on récupère sa réponse qui est de type HttpWebResponse .
  • lignes 41-42 : on utilise un StreamReader pour lire chaque ligne de la réponse html du serveur.
  • lignes 45-53 : dans chaque ligne de la réponse, on cherche la traduction. Lorsqu'on l'a trouvée, on arrête de lire la réponse Html et on ferme tous les flux qu'on a ouverts.
  • lignes 55-57 : si on n'a pas trouvé de traduction dans la réponse html, on prépare une exception de type WebTraductionsException pour le dire.
  • lignes 60-62 : si une exception s'est produite lors de l'écange client / serveur, on l'encapsule dans une exception de type WebTraductionsException pour le dire.
  • lignes 64-68 : si une exception a été enregistrée, elle est lancée, sinon la traduction trouvée est rendue.

Notre exemple suppose que le proxy Http ne nécessite pas d'authentification. Si ce n'était pas le cas, on écrirait quelque chose comme :

Cacher Sélectionnez
  1. httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ;
  2. httpWebRequest.Proxy.Credentials=new NetworkCredential("login","password");

Nous avons utilisé ici WebRequest / WebResponse plutôt que WebClient parce que nous n'avons pas à exploiter la totalité de la réponse Html du serveur de traduction. Une fois la traduction trouvée dans cette réponse, nous n'avons plus besoin du reste des lignes de la réponse. La classe WebClient ne permet pas de faire cela.

Voici un programme de test de la classe ServiceTraduction :

Cacher Sélectionnez
  1. using System;
  2. using System.Collections.Generic;
  3. using dao;
  4. using entites;
  5.
  6. namespace ui {
  7.     class Program {
  8.         static void Main(string[] args) {
  9.             try {
  10.                 // création service traduction
  11.                 ServiceTraduction serviceTraduction = new ServiceTraduction();
  12.                 // expression régulière pour trouver la traduction
  13.                 serviceTraduction.RegexTraduction = @"<div class=""txtTrad"">(.*?)</div>";
  14.                 // url serveur de traduction
  15.                 serviceTraduction.UrlServeurTraduction = "http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}";
  16.                 // dictionnaire des langues traduites
  17.                 Dictionary<string, string> languesTraduites = new Dictionary<string, string>();
  18.                 languesTraduites["fe"]= "Français-Anglais";
  19.                 languesTraduites["fs"]= "Français-Espagnol";
  20.                 languesTraduites["ef"]= "Anglais-Français";
  21.                 serviceTraduction.LanguesTraduites = languesTraduites;
  22.                 // proxy
  23.                 //serviceTraduction.ProxyHttp = "pproxy.istia.uang:3128";
  24.                 // traduction
  25.                 string texte = "ce chien est perdu";
  26.                 string deQuoiVersQuoi = "fe";
  27.                 Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
  28.                 texte = "l'été sera chaud";
  29.                 deQuoiVersQuoi = "fs";
  30.                 Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
  31.                 texte = "my tailor is rich";
  32.                 deQuoiVersQuoi = "ef";
  33.                 Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
  34.                 texte = "xx";
  35.                 deQuoiVersQuoi = "ef";
  36.                 Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
  37.             } catch (WebTraductionsException e) {
  38.                 // erreur
  39.                 Console.WriteLine("L'erreur suivante de code {1} s'est produite : {0}", e.Message, e.Code);
  40.             }
  41.         }
  42.     }
  43. }

Les résultats obtenus sont les suivants :

Cacher Sélectionnez
  1. Traduction [Français-Anglais] de [ce chien est perdu] : [this dog is lost]
  2. Traduction [Français-Espagnol] de [l'été sera chaud] : [el verano será caliente]
  3. Traduction [Anglais-Français] de [my tailor is rich] : [mon tailleur est riche]
  4. Traduction [Anglais-Français] de [xx] : [xx]

Le projet [dao] de la solution est compilée en une DLL HttpTraductions.dll :

Image non disponible

XII-G-3-f. L'interface graphique de l'application

Revenons sur l'architecture de notre application :

Image non disponible

Nous écrivons maintenant la couche [ui]. Celle-ci fait l'objet du projet [ui] de la solution en construction :

Image non disponible

Le dossier [lib] [3] contient certaines des DLL référencées par le projet [4] :

  • celles nécessaires à Spring : Spring.Core , Common.Logging, antlr.runtime
  • celle de la couche [dao] : HttpTraductions

Le fichier [App.config] contient la configuration Spring :

Cacher Sélectionnez
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.
  4.     <configSections>
  5.         <sectionGroup name="spring">
  6.             <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
  7.             <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
  8.         </sectionGroup>
  9.     </configSections>
  10.
  11.     <spring>
  12.         <context>
  13.             <resource uri="config://spring/objects" />
  14.         </context>
  15.         <objects xmlns="http://www.springframework.net">
  16.             <description>Traductions sur le web</description>
  17.             <!-- le service de traduction -->
  18.             <object name="ServiceTraduction" type="dao.ServiceTraduction, HttpTraductions">
  19.                 <property name="UrlServeurTraduction" value="http://trans.voila.fr/traduction_voila.php?isText=1&amp;translationDirection={0}&amp;stext={1}"/>
  20.                 <!--
  21.                 <property name="ProxyHttp" value="pproxy.istia.uang:3128"/>
  22.                 -->
  23.                 <property name="RegexTraduction" value="&lt;div class=&quot;txtTrad&quot;&gt;(.*?)&lt;/div&gt;"/>
  24.                 <property name="LanguesTraduites">
  25.                     <dictionary key-type="string" value-type="string">
  26.                         <entry key="fe" value="Français-Anglais"/>
  27.                         <entry key="ef" value="Anglais-Français"/>
  28. ...
  29.                         <entry key="ei" value="Anglais-Italien"/>
  30.                         <entry key="ie" value="Italien-Anglais"/>
  31.                     </dictionary>
  32.                 </property>
  33.             </object>
  34.         </objects>
  35.     </spring>
  36. </configuration>
  • ligne 15 : les objets à instancier par Spring. Il n'y en aura qu'un, celui de la ligne 18 qui instancie le service de traduction avec la classe ServiceTraduction trouvée dans la DLL HttpTraductions .
  • ligne 19 : la propriété UrlServeurTraduction de la classe ServiceTraduction. Il y a une difficulté avec le caractère & de l'Url. ce caractère a une signification dans un fichier Xml. Il doit donc être protégé. C'est le cas également d'autres caractères que nous allons rencontrer dans la suite du fichier. Ils doivent être remplacés par une séquence [ &code;] : & par [ &amp;] , < par [ &lt;] > par [ &gt ;], " par [ &quot;].
  • ligne 21 : la propriété ProxyHttp de la classe ServiceTraduction. Une propriété non initialisée reste à null . Ne pas définir cette propriété revient à dire qu'il n'y a pas de proxy Http.
  • ligne 23 : la propriété RegexTraduction de la classe ServiceTraduction. Dans l'expression régulière, il a fallu remplacer les caractères [< > "] par leurs équivalents protégés.
  • lignes 24-33 : la propriété LanguesTraduites de la classe ServiceTraduction.

Le programme [Program.cs] est exécuté au démarrage de l'application. Son code est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Text;
  3. using System.Windows.Forms;
  4. using dao;
  5. using Spring.Context;
  6. using Spring.Context.Support;
  7.
  8. namespace ui {
  9.     static class Program {
  10.         /// <summary>
  11.         /// The main entry point for the application.
  12.         /// </summary>
  13.         [STAThread]
  14.         static void Main() {
  15.             Application.EnableVisualStyles();
  16.             Application.SetCompatibleTextRenderingDefault(false);
  17.
  18.             // --------------- Code développeur
  19.             // instanciation service de traduction
  20.             IApplicationContext ctx = null;
  21.             Exception ex = null;
  22.             ServiceTraduction serviceTraduction = null;
  23.             try {
  24.                 // contexte Spring
  25.                 ctx = ContextRegistry.GetContext();
  26.                 // on demande une référence sur le service de traduction
  27.                 serviceTraduction = ctx.GetObject("ServiceTraduction") as ServiceTraduction;
  28.             } catch (Exception e1) {
  29.                 // mémorisation exception
  30.                 ex = e1;
  31.             }
  32.             // formulaire à afficher
  33.             Form form = null;
  34.             // y-a-t-il eu une exception ?
  35.             if (ex != null) {
  36.                 // oui - on crée le message d'erreur à afficher
  37.                 StringBuilder msgErreur = new StringBuilder(String.Format("Chaîne des exceptions : {0}{1}", "".PadLeft(40, '-'), Environment.NewLine));
  38.                 Exception e = ex;
  39.                 while (e != null) {
  40.                     msgErreur.Append(String.Format("{0}: {1}{2}", e.GetType().FullName, e.Message, Environment.NewLine));
  41.                     msgErreur.Append(String.Format("{0}{1}", "".PadLeft(40, '-'), Environment.NewLine));
  42.                     e = e.InnerException;
  43.                 }
  44.                 // création fenêtre d'erreur à laquelle on passe le message d'erreur à afficher
  45.                 Form2 form2 = new Form2();
  46.                 form2.MsgErreur = msgErreur.ToString();
  47.                 // ce sera la fenêtre à afficher
  48.                 form = form2;
  49.             } else {
  50.                 // tout s'est bien passé
  51.                 // création interface graphique [Form1] à laquelle on passe la référence sur le sevice de traduction
  52.                 Form1 form1 = new Form1();
  53.                 form1.ServiceTraduction = serviceTraduction;
  54.                 // ce sera la fenêtre à afficher
  55.                 form = form1;
  56.             }
  57.             // affichage fenêtre
  58.             Application.Run(form);
  59.         }
  60.     }
  61. }

Ce code a déjà été utilisé dans l'application Impôts version 6, au paragraphe , page .

  • le service de traduction est créé ligne 27 par Spring. Si cette création s'est bien passée, le formulaire [Form1] sera affiché (lignes 52-55), sinon c'est le formulaire d'erreur [Form2] qui le sera (lignes 36-48).

Le formulaire [Form2] est celui utilisé dans l'application Impôts version 6 et a été expliqué au paragraphe , page .

Le formulaire [Form1] est le suivant :

Image non disponible
n° | type | nom | rôle
1 | TextBox | textBoxTexteATraduire | boîte de saisie du texte à traduire MultiLine=true
2 | ComboBox | comboBoxLangues | la liste des sens de traduction
3 | Button | buttonTraduire | pour demander la traduction du texte [1] dans le sens [2]
4 | TextBox | textBoxTraduction | la traduction du texte [1]

Le code du formulaire [Form1] est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Windows.Forms;
  5. using dao;
  6.
  7. namespace ui {
  8.     public partial class Form1 : Form {
  9.         // service de traduction
  10.         public ServiceTraduction ServiceTraduction { get; set; }
  11.         // dictionnaire des langues
  12.         Dictionary<string, string> languesInversées = new Dictionary<string, string>();
  13.
  14.         // constructeur
  15.         public Form1() {
  16.             InitializeComponent();
  17.         }
  18.
  19.         // chargement initial du formulaire
  20.         private void Form1_Load(object sender, EventArgs e) {
  21.             // construction du dictionnaire inversé des langues
  22.             foreach (string code in ServiceTraduction.LanguesTraduites.Keys) {
  23.                 // langues
  24.                 string langues = ServiceTraduction.LanguesTraduites[code];
  25.                 // ajout (langues, code) au dictionnaire inversé
  26.                 languesInversées[langues] = code;
  27.             }
  28.             // remplissage combo dans l'ordre alphabétique des langues
  29.             string[] languesCombo = languesInversées.Keys.ToArray();
  30.             Array.Sort<string>(languesCombo);
  31.             foreach (string langue in languesCombo) {
  32.                 comboBoxLangues.Items.Add(langue);
  33.             }
  34.             // sélection 1ère langue
  35.             if (comboBoxLangues.Items.Count != 0) {
  36.                 comboBoxLangues.SelectedIndex = 0;
  37.             }
  38.         }
  39.
  40.         private void buttonTraduire_Click(object sender, EventArgs e) {
  41.             // qq chose a traduire ?
  42.             string texte = textBoxTexteATraduire.Text.Trim();
  43.             if (texte == "") return;
  44.             // traduction
  45.             try {
  46.                 textBoxTraduction.Text = ServiceTraduction.Traduire(texte, languesInversées[comboBoxLangues.SelectedItem.ToString()]);
  47.             } catch (Exception ex) {
  48.                 textBoxTraduction.Text = ex.Message;
  49.             }
  50.         }
  51.     }
  52. }
  • ligne 10 : une référence sur le service de traduction. Cette propriété publique a été initialisée par [Program.cs], ligne 53. Lorsque les méthodes Form1_Load (ligne 20) ou buttonTraduire_Click (ligne 40) s'exécutent, ce champ est donc déjà initialisé.
  • ligne 12 : le dictionnaire des langues traduites avec des entrées de type ["Français-Anglais","fe"], c.a.d. l'inverse du dictionnaire LanguesTraduites rendu par le service de traduction.
  • ligne 20 : la méthode Form1_Load s'exécute au chargement du formulaire.
  • lignes 22-27 : on utilise le dictionnaire serviceTraduction.LanguesTraduites ["fe","Français-Anglais"] pour construire le dictionnaire languesInversées ["Français-Anglais", "fe"].
  • ligne 29 : languesCombo est le tableau des clés du dictionnaire languesInversées , c.a.d. un tableau d'éléments ["Français-Anglais"]
  • ligne 30 : ce tableau est trié afin de présenter dans le combo les sens de traduction par ordre alphabétique
  • lignes 31-33 : le combo des langues est rempli.
  • ligne 40 : la méthode exécutée lorsque l'utilisateur clique sur le bouton [Traduire]
  • ligne 46 : il suffit d'appeler la méthode serviceTraduction.Traduire pour demander la traduction. Le 1er paramètre est le texte à traduire, le second le code du sens de traduction. Ce code est trouvé dans le dictionnaire languesInversées à partir de l'élément sélectionné dans le combo des langues.
  • ligne 48 : s'il y a une exception, elle est affichée à la place de la traduction.

XII-G-3-g. Conclusion

Cette application a montré que les clients web du framework .NET nous permettaient d'exploiter les ressources du web. La technique est à chaque fois similaire :

  • déterminer l'Uri à interroger. Cette Uri est la plupart du temps paramétrée.
  • l'interroger
  • trouver dans la réponse du serveur ce qu'on cherche grâce à des expressions régulières

Cette technique est aléatoire. En effet au fil du temps, l'Uri interrogée ou l'expression régulière permettant de trouver le résultat attendu peuvent changer. On a donc intérêt à placer ces deux informations dans un fichier de configuration. Mais cela peut se révéler insuffisant. Nous verrons dans le chapitre suivant qu'il existe des ressources plus stables sur le web : les services web.

XII-G-4. Un client SMTP (Simple Mail Tranport Protocol) avec la classe SmtpClient

Un client SMTP est un client d'un serveur SMTP, serveur d'envoi de courrier. La class .NET SmtpClient encapsule totalement les besoins d'un tel client. Le développeur n'a pas à connaître les détails du protocole SMTP. Nous connaissons ce dernier. Il a été présenté au paragraphe , page .

Nous présentons la classe SmtpClient dans le cadre d'une application windows basique qui permet d'envoyer des courriers électroniques avec pièces jointes. L'application va se connecter au port 25 d'un serveur SMTP. On rappelle que sur la plupart des PC windows, les pare-feu ou autres antivirus bloquent les connexions vers le port 25. Il est alors nécessaire de désactiver cette protection pour tester l'application :

Image non disponible

Le client Smtp aura une architecture monocouche :

Image non disponible

Le projet Visual studio est le suivant :

Image non disponible

L'interface graphique [SendMailForm.cs] de l'application est la suivante :

Image non disponible
n° | type | nom | rôle
1 | TextBox | textBoxServeur | nom du serveur SMTP auquel se connecter
2 | NumericUpDown | numericUpDownPort | le port sur lequel se connecter
3 | TextBox | textBoxExpediteur | adresse de l'expéditeur du message
4 | TextBox | textBoxTo | adresses des destinataires sous la forme : adresse1,adresse2…
5 | TextBox | textBoxCc | adresses des destinataires en copie (CC=Carbon Copy) sous la forme : adresse1,adresse2…
6 | TextBox | textBoxBcc | adresses des destinataires en copie aveugle (BCC=Blind Carbon Copy) sous la forme : adresse1,adresse2… Toutes les adresses de ces trois champs de saisie recevront le même message avec les mêmes attachements. Les personnes destinataires du message pourront connaître les adresses qui étaient dans les champs 4 et 5 mais pas celles du champ 6. Le Bcc est donc une façon de mettre quelqu'un en copie sans que les autres destinataires du message le sachent.
7 | Button | buttonAjouter | pour ajouter une pièce jointe au courrier
8 | ListBox | listBoxPiecesJointes | liste des pièces à joindre au courrier
9 | TextBox | textBoxSujet | sujet du courrier
10 | TextBox | textBoxMessage | le texte du message. MultiLine=true
11 | Button | buttonEnvoyer | pour envoyer le message et les éventuelles pièces jointes
12 | TextBox | textBoxRésultat | affiche un résumé du message envoyé ou bien un message d'erreur si un problème a été rencontré
13 | Button | buttonEffacer | pour effacer [12]
 | OpenfileDialog | openFileDialog1 | contrôle non visuel qui permet le choix d'une pièce jointe dans le système de fichiers local

Dans l'exemple précédent, le résumé affiché en [12] est le suivant :

Cacher Sélectionnez
  1. Envoi réussi...
  2. Sujet : votre demande
  3. Destinataires : y2000@hotmail.com
  4. Cc :
  5. Bcc :
  6. Pièces jointes :
  7. C:\data\travail\2007-2008\recrutements 0809\ing3\documents\ing3.zip
  8. Texte : Bonjour,
  9.
  10. Vous trouverez ci-joint le dossier de candidature à l'ISTIA.
  11.
  12. Cordialement,
  13.
  14. ST

Le code du formulaire [SendMailForm.cs] est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Windows.Forms;
  3. using System.Net.Mail;
  4. using System.Text.RegularExpressions;
  5. using System.Text;
  6.
  7. namespace Chap9 {
  8.     public partial class SendMailForm : Form {
  9.         public SendMailForm() {
  10.             InitializeComponent();
  11.         }
  12.
  13.         // ajout d'une pièce jointe
  14.         private void buttonAjouter_Click(object sender, EventArgs e) {
  15.             // on paramètre la boîte de dialogue openfileDialog1
  16.             openFileDialog1.InitialDirectory = Application.ExecutablePath;
  17.             openFileDialog1.Filter = "Tous les fichiers (*.*)|*.*";
  18.             openFileDialog1.FilterIndex = 0;
  19.             openFileDialog1.FileName = "";
  20.             // on affiche la boîte de dialogue et on récupère son résultat
  21.             if (openFileDialog1.ShowDialog() == DialogResult.OK) {
  22.                 // on récupère le nom du fichier
  23.                 listBoxPiecesJointes.Items.Add(openFileDialog1.FileName);
  24.             }
  25.         }
  26.
  27.         private void textBoxServeur_TextChanged(object sender, EventArgs e) {
  28.             setStatutEnvoyer();
  29.         }
  30.
  31.         private void setStatutEnvoyer() {
  32.             buttonEnvoyer.Enabled = textBoxServeur.Text.Trim() != "" && textBoxTo.Text.Trim() != "" && textBoxSujet.Text.Trim() != "";
  33.         }
  34.
  35.         // retirer une pièce jointe
  36.         private void buttonRetirer_Click(object sender, EventArgs e) {
  37.             // pièce jointe sélectionnée ?
  38.             if (listBoxPiecesJointes.SelectedIndex != -1) {
  39.                 // on la retire
  40.                 listBoxPiecesJointes.Items.RemoveAt(listBoxPiecesJointes.SelectedIndex);
  41.                 // on met à jour le bouton Retirer
  42.                 buttonRetirer.Enabled = listBoxPiecesJointes.Items.Count != 0;
  43.             }
  44.         }
  45.
  46.         private void listBoxPiecesJointes_SelectedIndexChanged(object sender, EventArgs e) {
  47.             // pièce jointe sélectionnée ?
  48.             if (listBoxPiecesJointes.SelectedIndex != -1) {
  49.                 // on met à jour le bouton Retirer
  50.                 buttonRetirer.Enabled = true;
  51.             }
  52.         }
  53.
  54.         // envoi du message avec ses attachements
  55.         private void buttonEnvoyer_Click(object sender, EventArgs e) {
  56. ....
  57.         }
  58.
  59.         private void textBoxTo_TextChanged(object sender, EventArgs e) {
  60.             setStatutEnvoyer();
  61.         }
  62.
  63.         private void textBoxSujet_TextChanged(object sender, EventArgs e) {
  64.             setStatutEnvoyer();
  65.         }
  66.
  67.         private void buttonEffacer_Click(object sender, EventArgs e) {
  68.             textBoxResultat.Text = "";
  69.         }
  70.     }
  71. }

Nous ne commenterons pas ce code qui ne présente pas de nouveautés. Pour comprendre la méthode buttonAjouter_Click de la ligne 14, le lecteur est invité à relire le paragraphe , de la page .

La méthode buttonEnvoyer_Click de la ligne 55, qui envoie le courrier est la suivante :

Cacher Sélectionnez
  1. private void buttonEnvoyer_Click(object sender, EventArgs e) {
  2.             try {
  3.                 // sablier
  4.                 Cursor = Cursors.WaitCursor;
  5.                 // le client Smtp
  6.                 SmtpClient smtpClient = new SmtpClient(textBoxServeur.Text.Trim(), (int)numericUpDownPort.Value);
  7.                 // le message
  8.                 MailMessage message = new MailMessage();
  9.                 // expéditeur
  10.                 message.Sender = new MailAddress(textBoxExpéditeur.Text.Trim());
  11.                 message.From = message.Sender;
  12.                 // destinataires
  13.                 Regex marqueur = new Regex("\\s*,\\s*");
  14.                 string[] destinataires = marqueur.Split(textBoxTo.Text.Trim());
  15.                 foreach (string destinataire in destinataires) {
  16.                     if (destinataire.Trim() != "") {
  17.                         message.To.Add(new MailAddress(destinataire));
  18.                     }
  19.                 }
  20.                 // CC
  21.                 string[] copies = marqueur.Split(textBoxCc.Text.Trim());
  22.                 foreach (string copie in copies) {
  23.                     if (copie.Trim() != "") {
  24.                         message.CC.Add(new MailAddress(copie));
  25.                     }
  26.                 }
  27.                 // BCC
  28.                 string[] blindCopies = marqueur.Split(textBoxBcc.Text.Trim());
  29.                 foreach (string blindCopie in blindCopies) {
  30.                     if (blindCopie.Trim() != "") {
  31.                         message.Bcc.Add(new MailAddress(blindCopie));
  32.                     }
  33.                 }
  34.                 // sujet
  35.                 message.Subject = textBoxSujet.Text.Trim();
  36.                 // texte du message
  37.                 message.Body = textBoxMessage.Text;
  38.                 // attachements
  39.                 foreach (string attachement in listBoxPiecesJointes.Items) {
  40.                     message.Attachments.Add(new Attachment(attachement));
  41.                 }
  42.                 // envoi du message
  43.                 smtpClient.Send(message);
  44.                 // Ok - on affiche un résumé
  45.                 StringBuilder msg = new StringBuilder(String.Format("Envoi réussi...{0}", Environment.NewLine));
  46.                 msg.Append(String.Format("Sujet : {0}{1}", textBoxSujet.Text.Trim(), Environment.NewLine));
  47.                 textBoxSujet.Clear();
  48.                 msg.Append(String.Format("Destinataires : {0}{1}", textBoxTo.Text.Trim(), Environment.NewLine));
  49.                 textBoxTo.Clear();
  50.                 msg.Append(String.Format("Cc : {0}{1}", textBoxCc.Text.Trim(), Environment.NewLine));
  51.                 textBoxCc.Clear();
  52.                 msg.Append(String.Format("Bcc : {0}{1}", textBoxBcc.Text.Trim(), Environment.NewLine));
  53.                 textBoxBcc.Clear();
  54.                 msg.Append(String.Format("Pièces jointes :{0}", Environment.NewLine));
  55.                 foreach (string attachement in listBoxPiecesJointes.Items) {
  56.                     msg.Append(String.Format("{0}{1}", attachement, Environment.NewLine));
  57.                 }
  58.                 msg.Append(String.Format("Texte : {0}{1}", textBoxMessage.Text, Environment.NewLine));
  59.                 listBoxPiecesJointes.Items.Clear();
  60.                 textBoxResultat.Text = msg.ToString();
  61.             } catch (Exception ex) {
  62.                 // on affiche l'erreur
  63.                 textBoxResultat.Text = String.Format("L'erreur suivante s'est produite {0}", ex);
  64.             }
  65.             // curseur normal
  66.             Cursor = Cursors.Arrow;
  67.         }
  • ligne 6 : le client Smtp est créé. Il a besoin de deux paramètres : le nom du serveur SMTP et le port sur lequel celui-ci opère
  • ligne 8 : un message de type MailMessage est créé. C'est lui qui va encapsuler la totalité du message à envoyer.
  • ligne 10 : l'adresse électronique Sender de l'expéditeur est renseignée. Une adresse électronique est une instance de type MailAddress construite à patir d'une chaîne de caractères "xx@yy.zz". Il faut que cette chaîne ait la forme attendue pour une adresse électronique, sinon une exception est lancée. Dans ce cas, elle sera afichée dans le champ textBoxResultat (ligne 63) sous une forme peu conviviale.
  • lignes 13-19 : les adresses électroniques des destinataires sont placées dans la liste To du messsage. On récupère ces adresses dans le champ textBoxTo . L'expression régulière de la ligne 13 permet de récupérer les différentes adresses qui sont séparées par une virgule.
  • lignes 21-26 : on répète le même processus pour initialiser le champ CC du messsage avec les adresses en copie du champ textBoxCc .
  • lignes 28-33 : on répète le même processus pour initialiser le champ Bcc du messsage avec les adresses en copie aveugle du champ textBoxBcc .
  • ligne 35 : le champ Subject du messsage est initialisé avec le sujet du champ textBoxSujet .
  • ligne 37 : le champ Body du messsage est initialisé avec le texte du message textBoxMessage .
  • lignes 39-41 : les pièces jointes sont attachées au message. Chaque pièce jointe est ajoutée sous forme d'un objet Attachment au champ Attachments du message. Un objet Attachment est instancié à partir du chemin complet de la pièce à attacher dans le système de fichiers local.
  • ligne 43 : le message est envoyé à l'aide de la méthode Send du client Smtp.
  • lignes 45-60 : écriture du résumé de l'envoi dans le champ textBoxResultat et réinitialisation du formulaire.
  • ligne 63 : affichage d'une éventuelle erreur

XII-H. Un client Tcp générique asynchrone

XII-H-1. Présentation

Dans tous les exemples de ce chapitre, la communication client / serveur se faisait en mode bloquant qu'on appelle également mode synchrone :

  • lorsqu'un client se connecte à un serveur, il attend la réponse du serveur à cette demande avant de continuer.
  • lorsqu'un client lit une ligne de texte envoyée par le serveur, il est bloqué tant que le serveur n'a pas envoyé celle-ci.
  • côté serveur, les threads de service qui assurent le service au client fonctionnent de la même façon que ci-dessus.

Dans les interfaces graphiques, il est souvent nécessaire de ne pas bloquer l'utilisateur sur des opérations longues. Le cas souvent cité est celui du téléchargement d'un gros fichier. Pendant ce téléchargement, il faut laisser l'utilisateur libre de continuer à interagir avec l'interface graphique.

Nous nous proposons ici de réécrire le client Tcp générique du paragraphe , page en y apportant les changements suivants :

  • l'interface sera graphique
  • l'outil de communication avec le serveur sera un objet Socket
  • le mode de communication sera asynchrone :
  • le client initiera une connexion au serveur mais ne restera pas bloqué à attendre qu'elle soit établie
  • le client initiera un envoi au serveur mais ne restera pas bloqué à attendre qu'il soit terminé
  • le client initiera la réception de données provenant du serveur mais ne restera pas bloqué à attendre la fin de celle-ci.

Rappelons à quel niveau se situe l'objet Socket dans la communication client / serveur Tcp :

Image non disponible

La classe Socket est celle qui opère le plus près du réseau. Elle permet de gérer finement la connexion réseau. Le terme socket désigne une prise de courant. Le terme a été étendu pour désigner une prise de réseau logicielle. Dans une communication TCP-IP entre deux machines A et B, ce sont deux sockets qui communiquent entre-eux. Une application peut travailler directement avec les sockets . C'est le cas de l'application A ci-dessus. Un socket peut être un socket client ou serveur .

XII-H-2. L'interface graphique du client Tcp asynchrone

L'application Visual studio est la suivante :

Image non disponible

[ClientTcpAsynchrone.cs] est l'interface graphique. Celle-ci est la suivante :

Image non disponible
n° | type | nom | rôle
1 | TextBox | textBoxNomServeur | nom du serveur Tcp auquel se connecter
2 | NumericUpDown | numericUpDownPortServeur | le port sur lequel se connecter
3 | RadioButton | radioButtonLF radioButtonRCLF | pour indiquer la marque de fin de ligne que le client doit utiliser : LF "\n" ou RCLF "\r\n"
4 | Button | buttonConnexion | pour se connecter sur le port [2] du serveur [1]. Le bouton a le libellé [Connecter] lorsque le client n'est pas connecté à un serveur, [Déconnecter] lorsqu'il est connecté.
5 | TextBox | textBoxMsgToServeur | message à envoyer au serveur une fois la connexion faite. Lorsque l'utilisateur tape sur la touche [Entrée], le message est envoyé avec la marque de fin de ligne choisie en [3]
6 | ListBox | listBoxEvts | liste dans laquelle sont affichés les principaux événements de la liaison client / serveur : connexion, déconnexion, fermeture de flux, erreurs de communication
7 | ListBox | listBoxDialogue | liste dans laquelle sont affichés les messages du dialogue client / serveur
8 | Button | buttonRazEvts | pour effacer la liste [6]
4 | Button | buttonRazDialogue | pour effacer la liste [7]

Les principes de fonctionnent de cette interface sont les suivants :

  • l'utilisateur connecte son client Tcp graphique à un service Tcp grâce à [1, 2, 3, 4].
  • un thread asynchrone accepte en continu toutes les données envoyées par le serveur Tcp et les affiche dans la liste [7]. Ce thread est dissocié des autres activités de l'interface.
  • l'utilisateur peut envoyer à son rythme des messages au serveur grâce à [5]. Chaque message est envoyé par un thread asynchrone. A la différence du thread de réception qui ne s'arrête jamais, le thread d'émission est lui terminé dès que le message a été envoyé. Un nouveau thread asynchrone sera utilisé pour le message suivant.
  • la communication client / serveur se termine lorsque l'un des partenaires clôt la connexion. L'utilisateur peut prendre cette initiative avec le bouton [4] qui une fois la connexion établie a le libellé [Déconnecter].

Voici une copie d'écran d'une exécution :

Image non disponible
  • en [1] : connexion à un service POP
  • en [2] : affichage des événements ayant eu lieu lors de la connexion
  • en [3] : le message envoyé par le serveur POP à l'issue de la connexion
  • en [4] : le bouton [Connecter] est devenu le bouton [Déconnecter]
Image non disponible
  • en [1], on a envoyé la commande quit au serveur POP. Le serveur a répondu +OK goodbye et a fermé la connexion
  • en [2], cette fermeture côté serveur a été détectée. Le client a alors fermé la connexion de son côté.
  • en [3], le bouton [Déconnecter] est redevenu un bouton [Connecter]

XII-H-3. Connexion asynchrone au serveur

L'appui sur le bouton [Connecter] provoque l'exécution de la méthode suivante :

Cacher Sélectionnez
  1.         private void buttonConnexion_Click(object sender, EventArgs e) {
  2.             // connexion ou déconnexion ?
  3.             if (buttonConnexion.Text == "Déconnecter")
  4.                 déconnexion();
  5.             else
  6.                 connexion();
  7. }
  • ligne 3 : le bouton peut avoir le libellé [Connecter] ou [Déconnecter].

La méthode de connexion est la suivante :

Cacher Sélectionnez
  1. using System.Net.Sockets;
  2. ...
  3.
  4. namespace Chap9 {
  5.     public partial class ClientTcp : Form {
  6.         const int tailleBuffer = 1024;
  7.         private Socket client = null;
  8.         private byte[] data = new byte[tailleBuffer];
  9.         private string réponse = null;
  10.         private string finLigne = "\r\n";
  11.
  12.         // délégués
  13.         public delegate void writeLog(string log);
  14.
  15.         public ClientTcp() {
  16.             InitializeComponent();
  17.         }
  18. ....................................
  19.     private void connexion() {
  20.             // vérifications données
  21.             string nomServeur = textBoxNomServeur.Text.Trim();
  22.             if (nomServeur == "") {
  23.                 logEvent("indiquez le nom du serveur");
  24.                 return;
  25.             }
  26.             // suivi
  27.             logEvent(String.Format("connexion en cours au serveur {0}", nomServeur));
  28.             try {
  29.                 // création socket
  30.                 client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
  31.                 // connexion asynchrone
  32.                 client.BeginConnect(Dns.GetHostEntry(nomServeur).AddressList[0],(int)numericUpDownPortServeur.Value, connecté, client);
  33.
  34.             } catch (Exception ex) {
  35.                 logEvent(String.Format("erreur de connexion : {0}", ex.Message));
  36.                 return;
  37.             }
  38.         }
  39.
  40.         // la connexion a eu lieu
  41.         private void connecté(IAsyncResult résultat) {
  42.             // on récupère le socket du client
  43.             Socket client = résultat.AsyncState as Socket;
  44.     ...
  45.         }
  46.
  47.
  48.         // suivi du processus
  49.         private void logEvent(string msg) {
  50. ....
  51.         }
  52.     }
  53. }
  • ligne 1 : la classe Socket fait partie de l'espace de noms System.Net.Sockets .

Un certain nombre de données doivent être partagées entre plusieurs méthodes du formulaire. Ce sont les suivantes :

  • ligne 7 : client est le socket de communication avec le serveur
  • lignes 6 et 8 : le client va recevoir ses messages dans un tableau d'octets data.
  • ligne 9 : réponse est la réponse envoyée par le serveur.
  • ligne 10 : finLigne est la marque de fin de ligne utilisée par le client Tcp - est initialisée par défaut à RCLF mais peut être modifiée par l'utilisateur avec les boutons radio [3].

La procédure connexion de la ligne 19 réalise la connexion au serveur Tcp :

  • lignes 21-25 : on vérifie que le nom du serveur est non vide. Si ce n'est pas le cas, l'événement est logué dans listBoxEvts par la méthode logEvent de la ligne 49.
  • ligne 27 : on signale que la connexion va avoir lieu
  • ligne 30 : on crée l'objet Socket nécessaire à la communication Tcp-Ip. Le constructeur admet trois paramètres :
  • AddressFamily addressFamily : la famille des adresses IP du client et du serveur, ici des adresses IPv4 ( AddressFamily.InterNetwork )
  • SocketType socketType : le type du socket. Le type SocketType.Stream est adapté aux connexions Tcp-Ip
  • ProtocolType protocolType : le type de protocole internet utilisé, ici le protocole Tcp
  • ligne 32 : la connexion est faite de façon asynchrone. La connexion est lancée mais l'exécution continue sans en attendre la fin. La méthode [Socket].BeginConnect admet quatre paramètres :
  • IPAddress ipAddress : l'adresse Ip de la machine sur laquelle s'exécute le service auquel il faut se connecter
  • Int32 port : le port du service
  • AsyncCallBack asyncCallBack : AsyncCallBack est un type delegate :
Cacher Sélectionnez
  1. public void AsyncCallBack(IAsyncResult ar);

La méthode asyncCallBack passée en 3ième paramètre de la méthode BeginConnect doit être une méthode acceptant un type IAsyncCallBack et ne rendant aucun résultat. C'est la méthode qui sera appelée lorsque la connexion aura été faite. Nous passons ici comme 3ième paramètre, la méthode connecté de la ligne 41.

  • Object state : un objet à passer à la méthode asyncCallBack . Cette méthode reçoit (cf délégué ci-dessus) un paramètre ar de type IAsyncResult. L'objet state pourra être récupéré dans ar.AsyncState (ligne 43). Nous passons ici comme 4ième paramètre le socket du client.
  • ligne 38 : la méthode est terminée. L'utilisateur peut de nouveau interagir avec l'interface graphique. La connexion se passe en tâche de fond, en parallèle de la gestion des événements de l'interface graphique. Toujours en parallèle, la méthode connecté de la ligne 41 va être appelée à la fin de la connexion, que celle-ci se termine bien ou mal.

Le code de la méthode connecté est le suivant :

Cacher Sélectionnez
  1. // la connexion a eu lieu
  2.         private void connecté(IAsyncResult résultat) {
  3.             // on récupère la socket du client
  4.             Socket client = résultat.AsyncState as Socket;
  5.             try {
  6.                 // on termine l'opération asynchrone
  7.                 client.EndConnect(résultat);
  8.                 // suivi
  9.                 logEvent(String.Format("connecté au service {0}", client.RemoteEndPoint));
  10.                 // formulaire
  11.                 buttonConnexion.Text = "Déconnecter";
  12.                 // lecture asynchrone des données en provenance du serveur
  13.                 réponse = "";
  14.                 client.BeginReceive(data, 0, tailleBuffer, SocketFlags.None, lecture, client);
  15.             } catch (SocketException e) {
  16.                 logEvent(String.Format("erreur de connexion : {0}", e.Message));
  17.                 return;
  18.             }
  19. }
  20.
  21.         // réception de données
  22.         private void lecture(IAsyncResult résultat) {
  23.             // on récupère la socket du client
  24.             Socket client = résultat.AsyncState as Socket;
  25. ...
  26.         }
  • ligne 4 : le socket du client est récupéré dans le paramètre résultat reçu par la méthode. On rappelle que cet objet est celui passé comme 4ième paramètre de la méthode BeginConnect .
  • ligne 7 : la tentative de connexion est terminée par la méthode EndConnect à qui on doit passer le paramètre résultat reçu par la méthode.
  • ligne 9 : l'événement est logué dans la liste des événements
  • ligne 11 : le bouton [Connecter] devient un bouton [Déconnecter] pour que l'utilisateur puisse demander la déconnexion.
  • ligne 13 : la réponse du serveur est initialisée. Elle va être mise à jour par des appels répétés à la méthode asynchrone BeginReceive .
  • ligne 14 : 1er appel à la méthode asynchrone BeginReceive . Celle-ci est appelée avec les paramètres suivants :
  • byte[] buffer : le tampon dans lequel placer les données qui vont être reçues - ici le buffer est data
  • int offset : à partir de quelle position du tampon placer les données les données qui vont être reçues - ici l'offset est 0, c.a.d. que les données sont placées dès le 1er octet du tampon.
  • int size : la taille en octets du tampon - ici la taille est tailleBuffer .
  • SocketFlags socketFlags : configuration du socket - ici aucune configuration
  • AsyncCallBack asyncCallBack : la méthode à rappeler lorsque la réception sera terminée. Ce sera la cas soit parce que le buffer a reçu des données soit parce que la connexion a été fermée. Ici, la méthode de rappel est la méthode lecture de la ligne 22.
  • Object state : l'objet à passer à la méthode de rappel asyncCallBack . Ici, on passe de nouveau le socket du client.

On notera que tout ceci se passe sans action de l'utilisateur, autre que la demande initiale de connexion avec le bouton [Connecter]. A la fin de la méthode connecté , une autre méthode est exécutée en tâche de fond : la méthode lecture que nous examinons maintenant.

Cacher Sélectionnez
  1. // réception de données
  2.         private void lecture(IAsyncResult résultat) {
  3.             // on récupère la socket du client
  4.             Socket client = résultat.AsyncState as Socket;
  5.             int nbOctetsReçus = 0;
  6.             bool erreur = false;
  7.             try {
  8.                 // nbre d'octets reçus
  9.                 nbOctetsReçus = client.EndReceive(résultat);
  10.                 if (nbOctetsReçus == 0) {
  11.                     // le serveur ne répond plus
  12.                     logEvent("le serveur a fermé la connexion");
  13.                 }
  14.             } catch (Exception e) {
  15.                 // on a eu un pb de réception
  16.                 logEvent(String.Format("erreur de réception : {0}", e.Message));
  17.                 erreur = true;
  18.             }
  19.             // terminé ?
  20.             if (nbOctetsReçus == 0 || erreur) {
  21.                 // on déconnecte au besoin le client
  22.                 déconnexion();
  23.                 // on affiche la fin de la réponse
  24.                 afficherRéponseServeur(réponse, true);
  25.                 // fin lecture
  26.                 return;
  27.             }
  28.             // on récupère les données reçues
  29.             string données = Encoding.UTF8.GetString(data, 0, nbOctetsReçus);
  30.             // on les ajoute aux données déjà recues
  31.             réponse += données;
  32.             // on affiche la réponse
  33.             afficherRéponseServeur(réponse, false);
  34.             // on continue à lire
  35.             client.BeginReceive(data, 0, tailleBuffer, SocketFlags.None, lecture, client);
  36.         }
  • ligne 2 : la méthode lecture se déclenche en tâche de fond lorsque le buffer data a reçu des données ou que la connexion a été fermée par le serveur.
  • ligne 9 : la demande asynchrone de lecture est terminée par EndReceive . Là encore cette méthode doit être appelée avec le paramètre reçu par la fonction de rappel. La méthode EndReceive rend le nombre d'octets reçus dans le buffer de lecture.
  • ligne 10 : si le nombre d'octets est nul c'est que la connexion a été fermée par le serveur.
  • ligne 12 : on note l'événement dans la liste des événements
  • ligne 14 : on traite une éventuelle exception
  • lignes 16-17 : on note l'événement dans la liste des événements et on note l'erreur
  • ligne 20 : on regarde si on doit fermer la connexion
  • ligne 22 : on ferme la connexion côté client avec une méthode déconnexion que nous verrons ultérieurement.
  • ligne 24 : la réponse du serveur, c.a.d. la variable globale réponse est affichée dans la liste de dialogue listBoxDialogue au moyen d'une méthode privée afficherRéponseServeur .
  • ligne 26 : fin de la méthode asynchrone lecture
  • ligne 29 : les octets reçus sont mis dans une chaîne de caractères au format UTF8.
  • ligne 31 : ils sont ajoutés à la réponse en cours de construction
  • ligne 33 : la réponse est affichée dans la liste listBoxDialogue .
  • ligne 35 : on se remet à attendre des données en provenance du serveur

En définitive, la méthode asynchrone lecture ne s'arrête jamais. De façon continue, elle lit les données en provenance du serveur et les fait afficher dans la liste listBoxDialogue . Elle ne s'arrête que lorsque la connexion est fermée soit par le serveur soit par l'utilisateur lui-même.

XII-H-4. Déconnexion du serveur

L'appui sur le bouton [Déconnecter] provoque l'exécution de la méthode suivante :

Cacher Sélectionnez
  1.         private void buttonConnexion_Click(object sender, EventArgs e) {
  2.             // connexion ou déconnexion ?
  3.             if (buttonConnexion.Text == "Déconnecter")
  4.                 déconnexion();
  5.             else
  6.                 connexion();
  7. }
  • ligne 3 : le bouton peut avoir le libellé [Connecter] ou [Déconnecter].

La méthode déconnexion assure la déconnexion du client :

Cacher Sélectionnez
  1. private void déconnexion() {
  2.             // fermeture socket
  3.             if (client != null && client.Connected) {
  4.                 try {
  5.                     // suivi
  6.                     logEvent(String.Format("déconnexion du service {0}", client.RemoteEndPoint));
  7.                     // déconnexion
  8.                     client.Shutdown(SocketShutdown.Both);
  9.                     client.Close();
  10.                     // formulaire
  11.                     buttonConnexion.Text = "Connecter";
  12.                 } catch (Exception ex) {
  13.                     // suivi
  14.                     logEvent(String.Format("erreur de lors de la déconnexion : {0}", ex.Message));
  15.                 }
  16.             }
  17.         }
  • ligne 3 : si le client existe et est connecté
  • ligne 6 : on annonce la déconnexion dans listBoxEvts . La propriété client.RemoteEndPoint donne le couple (Adresse Ip, port) de l'autre extrémité de la connexion, c.a.d ici du serveur.
  • ligne 8 : le flux de données du socket est fermé avec la méthode ShutDown . Le flux de données d'un socket est bidirectionnel : le socket émet et reçoit des données. Le paramètre de la méthode ShutDown peut être : ShutDown.Receive pour fermer le flux de réception, Shutdonw.Send pour fermer le flux d'émission ou ShutDown.Both pour fermer les deux flux.
  • ligne 9 : on libère les ressources associées au socket
  • ligne 11 : le bouton [Déconnecter] devient le bouton [Connecter]
  • lignes 12-15 : gestion d'une éventuelle exception

XII-H-5. Envoi asynchrone de données au serveur

Lorsque l'utilisateur valide le message du champ textBoxMsgToServeur , la méthode suivante est exécutée :

Cacher Sélectionnez
  1.         private void textBoxMsgToServeur_KeyPress(object sender, KeyPressEventArgs e) {
  2.             // touche [Entrée] ?
  3.             if (e.KeyChar == 13 && client.Connected) {
  4.                 envoyerMessage();
  5.             }
  6. }
  • lignes 3-5 : si l'utilisateur a enfoncé la touche [Entrée] et si le socket du client est connecté, alors le message du champ textBoxMsgToServeur est envoyé avec la méthode envoyerMessage .

La méthode envoyerMessage est la suivante :

Cacher Sélectionnez
  1.         private void envoyerMessage() {
  2.             // envoyer un message de façon asynchrone
  3.             // le message
  4.             byte[] message = Encoding.UTF8.GetBytes(textBoxMsgToServeur.Text.Trim() + finLigne);
  5.             // il est envoyé
  6.             client.BeginSend(message, 0, message.Length, SocketFlags.None, écriture, client);
  7.             // dialogue
  8.             logDialogue("--> " + textBoxMsgToServeur.Text.Trim());
  9.             // raz message
  10.             textBoxMsgToServeur.Clear();
  11. }
  • ligne 4 : on ajoute au message la marque de fin de ligne du client et on le met dans le tableau d'octets message .
  • ligne 6 : une émission asynchrone est commencée avec la méthode BeginSend . Les paramètres de BeginSend sont identiques à ceux de la méthode BeginReceive . A la fin de l'opération d'émission asynchrone du message la méthode écriture sera appelée.
  • ligne 8 : le message envoyé est ajouté à la liste listBoxDialogue afin d'avoir un suivi du dialogue client / serveur
  • ligne 10 : le message envoyé est effacé de l'interface graphique

La méthode de rappel écriture est la suivante :

Cacher Sélectionnez
  1.         private void écriture(IAsyncResult résultat) {
  2.             // résultat de l'émission d'un message
  3.             Socket client = résultat.AsyncState as Socket;
  4.             try {
  5.                 client.EndSend(résultat);
  6.             } catch (Exception e) {
  7.                 // on a eu un pb d'émission
  8.                 logEvent(String.Format("erreur d'émission : {0}", e.Message));
  9.             }
  10. }
  • ligne 4 : la méthode de rappel écriture reçoit un paramètre résultat de type IAsyncResult .
  • ligne 3 : dans le paramètre résultat , on récupère le socket du client. Ce socket était le 5ième paramètre de la méthode BeginSend .
  • ligne 5 : on termine l'opération asynchrone d'émission.

On n'attend pas la fin de l'émission d'un message pour redonner la main à l'utilisateur. Celui-ci peut ainsi émettre un second message alors que l'émission du premier n'est pas terminé.

XII-H-6. Affichage des événements et du dialogue client / serveur

Les événements sont affichés par la méthode logEvents :

Cacher Sélectionnez
  1.         // suivi du processus
  2.         private void logEvent(string msg) {
  3.             listBoxEvts.Invoke(new writeLog(logEventCallBack), msg);
  4.         }
  5.
  6.         private void logEventCallBack(string msg) {
  7.             // affichage message
  8.             msg = msg.Replace(finLigne, " ");
  9.             listBoxEvts.Items.Insert(0, String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
  10. }
  • ligne 2 : la méthode logEvents reçoit en paramètre le message à ajouter dans la liste listBoxEvts .
  • ligne 3 : on ne peut utiliser directement le composant listBoxEvents . En effet, la méthode logEvents est appelée par deux types de threads :
  • le thread principal propriétaire de l'interface graphique, par exemple lorsqu'il signale qu'une tentative de connexion est en cours
  • un thread secondaire assurant une opération asynchrone. Ce type de thread n'est pas propriétaire des composants et son accès à un composant C doit être contrôlé par une opération C.Invoke . Cette opération indique au contrôle C qu'un thread veut faire une opération sur lui. La méthode Invoke admet deux paramètres :
  • une fonction de rappel de type delegate . Cette fonction de rappel sera exécutée par le thread propriétaire de l'interface graphique et non par le thread exécutant la méthode C.Invoke .
  • un objet qui sera passé à la fonction de rappel.

Ici le premier paramètre passé à la méthode Invoke est une instance du délégué suivant :

Cacher Sélectionnez
  1.         public delegate void writeLog(string log);

Le délégué writeLog a un paramètre de type string et ne rend aucun résultat. Le paramètre sera le message à enegistrer dans listBoxEvts .

Ligne 3, le premier paramètre passé à la méthode Invoke est la méthode logEventCallBack de la ligne 6. Elle correspond bien à la signature du délégué writeLog . Le second paramètre passé à la méthode Invoke est le message qui sera passé en paramètre à la méthode logEventCallBack .

L'opération Invoke est une opération synchrone. L'exécution du thread secondaire est bloquée jusqu'à ce que le thread propriétaire du contrôle exécute la méthode de rappel.

  • ligne 6 : la méthode de rappel exécutée par le thread de l'interface graphique reçoit le message à afficher dans le contrôle listBoxEvts .
  • ligne 9 : l'événement est logué en 1re position de la liste afin d'avoir les événements les plus récents en haut de la liste.

Les messages du dialogue client / serveur sont affichés par la méthode logDialogue :

Cacher Sélectionnez
  1.         // suivi du dialogue
  2.         private void logDialogue(string msg) {
  3.             listBoxDialogue.Invoke(new writeLog(logDialogueCallBack), msg);
  4.         }
  5.         private void logDialogueCallBack(string msg) {
  6.             // affichage message
  7.             msg = msg.Replace(finLigne, " ");
  8.             listBoxDialogue.Items.Add(String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
  9. }

Le principe est le même que dans la méthode logEvent .

Les messages reçus par le client sont affichés par la méthode afficherRéponseServeur :

Cacher Sélectionnez
  1.         private void afficherRéponseServeur(String msg, bool dernièreLigne) {
  2. ...
  3. }

Le permier paramètre est le message à afficher. Ce message peut être une suite de lignes. En effet le client lit les données en provenance du serveur par bloc de tailleBuffer (1024) octets. Dans ces 1024 octets, on peut trouver diverses lignes que l'on reconnaît à leur marque de fin de ligne "\n". La dernière ligne peut être incomplète, sa marque de fin de ligne étant dans les 1024 octets qui vont suivre. La méthode retrouve dans le message les lignes terminées par "\n" et demande ensuite à logDialogue de les afficher. Le second paramètre de la méthode indique s'il faut afficher la dernière ligne trouvée ou la laisser dans le buffer pour être complétée par le message suivant. Le code est assez complexe et ne présente pas d'intérêt ici. Aussi ne sera-t-il pas commenté.

XII-H-7. Conclusion

Le même exemple pourrait être traité avec des opérations synchrones. Ici l'aspect asynchrone de l'interface graphique apporte peu à l'utilisateur. Néanmoins, s'il se connecte et qu'ensuite il se rend compte que le serveur "ne répond plus", il a la possibilité de se déconnecter grâce au fait que l'interface graphique continue à répondre aux événements pendant l'exécution des opérations asynchrones. Cet exemple plutôt complexe nous a permis de présenter de nouvelles notions :

  • l'utilisation de sockets
  • l'utilisation de méthodes asynchrones. Ce qui a été vu fait partie d'un standard. D'autres méthodes asynchrones existent et fonctionnent sur le même modèle.
  • la mise à jour de contrôles d'une interface graphique par des threads secondaires.

La communication Tcp / Ip asynchrone présente des avantages plus sérieux pour un serveur que ceux affichés par l'exemple précédent. On sait que le serveur sert ses clients à l'aide de threads secondaires. Si son pool de threads a N threads, cela signifie qu'il ne peut servir que N clients simultanément. Si les N threads font tous une opération bloquante (synchrone), il n'y a plus de threads disponibles pour un nouveau client jusqu'à ce que l'une des opérations bloquantes s'achève et libère un thread. Si sur les threads, on fait des opérations asynchrones plutôt que synchrones, un thread n'est jamais bloqué et peut être rapidement recyclé pour de nouveaux clients.

XII-I. Application exemple, version 8 : Serveur de calcul d'impôts

XII-I-1. L'architecture de la nouvelle version

Nous reprenons l'application de calcul d'impôt déjà traitée sous diverses formes. Rappelons sa dernière mouture, celle de la version 7 du paragraphe , page .

Image non disponible

Les données étaient dans une base de données et la couche [ui] était une interface graphique :

Image non disponible

Nous allons rependre cette architecture et la distribuer sur deux machines :

Image non disponible
  • une machine [serveur] hébergera les couches [metier] et [dao] de la version 7. Une couche Tcp/Ip [serveur] [1] sera construite afin de permettre à des clients de l'internet d'interroger le service de calcul de l'impôt.
  • une machine [client] hébergera la couche [ui] de la version 7. Une couche Tcp/Ip [client] [2] sera construite afin de permettre à la couche [ui] d'interroger le service de calcul de l'impôt.

L'architecture change profondément ici. La version 7 était une application windows monoposte. La version 8 devient une application client / serveur de l'internet. Le serveur pourra servir plusieurs clients simultanément.

Nous allons tout d'abord écrire la partie [serveur] de l'application.

XII-I-2. Le serveur de calcul d'impôt

XII-I-2-a. Le projet Visual Studio

Image non disponible

Le projet Visual studio sera le suivant :

Image non disponible
  • en [1], le projet. On y trouve les éléments suivants :
  • [ServeurImpot.cs] : le serveur Tcp/Ip de calcul de l'impôt sous la forme d'une application console.
  • [dbimpots.sdf] : la base de données SQL Server compact de la version 7 décrite au paragraphe , page .
  • [App.config] : le fichier de configuration de l'application.
  • en [2], le dossier [lib] contient les DLL nécessaires au projet :
  • [ImpotsV7-dao] : la couche [dao] de la version 7
  • [ImpotsV7-metier] : la couche [metier] de la version 7
  • [antlr.runtime, CommonLogging, Spring.Core] pour Spring
  • en [3], les références du projet

XII-I-2-b. Configuration de l'application

Le fichier [App.config] est exploité par Spring. Son contenu est le suivant :

Cacher Sélectionnez
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.
  4.     <configSections>
  5.         <sectionGroup name="spring">
  6.             <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
  7.             <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
  8.         </sectionGroup>
  9.     </configSections>
  10.
  11.     <spring>
  12.         <context>
  13.             <resource uri="config://spring/objects" />
  14.         </context>
  15.         <objects xmlns="http://www.springframework.net">
  16.             <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
  17.                 <constructor-arg index="0" value="System.Data.SqlServerCe.3.5"/>
  18.                 <constructor-arg index="1" value="Data Source=|DataDirectory|\dbimpots.sdf;" />
  19.                 <constructor-arg index="2" value="select data1, data2, data3 from data"/>
  20.             </object>
  21.             <object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
  22.                 <constructor-arg index="0" ref="dao"/>
  23.             </object>
  24.         </objects>
  25.     </spring>
  26. </configuration>
  • lignes 16-20 : configuration de la couche [dao] associée à la base SQL Server compact
  • lignes 21-23 : configuration de la couche [metier].

C'est le fichier de configuration utilisée dans la couche [ui] de la version 7. Il a été présenté au paragraphe , page .

XII-I-2-c. Fonctionnement du serveur

Au démarrage du serveur, l'application serveur instancie les couches [metier] et [dao] puis affiche une interface console d'administration :

Image non disponible

La console d'administration accepte les commandes suivantes :

start port | pour lancer le service sur un port donné
stop | pour arrêter le service. Il peut être ensuite relancé sur le même port ou un autre.
echo start | pour activer l'écho du dialogue client / serveur sur la console
echo stop | pour désactiver l'écho
status | pour faire afficher l'état actif / inactif du service
quit | pour quitter l'application

Lançons le serveur :

Cacher Sélectionnez
  1. Serveur de calcul d'impôt >start 27
  2. Serveur de calcul d'impôt lancé sur le port 27
  3. Serveur de calcul d'impôt >

Lançons maintenant le client Tcp graphique asynchrone étudié précédemment au paragraphe , page .

Image non disponible

Le client est connecté. Il peut envoyer les commandes suivantes au serveur de calcul d'impôt :

aide | pour avoir la liste des commandes autorisées
impot marié nbEnfants salaireAnnuel | pour calculer l'impôt de quelqu'un ayant nbEnfants enfants et un salaire de salaireAnnuel euros. marié vaut o si la personne est mariée, n sinon.
aurevoir | pour clôre la connexion avec le serveur

Voici un exemple de dialogue :

Image non disponible

Côté serveur, la console affiche la chose suivante :

Cacher Sélectionnez
  1. Serveur de calcul d'impôt >start 27
  2. Serveur de calcul d'impôt >Serveur de calcul d'impôt lancé sur le port 27
  3. Début du service au client 0
  4. Fin du service au client 0

Mettons l'écho en route et recommençons un nouveau dialogue à partir du client graphique :

Image non disponible

La console d'administration affiche alors la chose suivante :

Cacher Sélectionnez
  1. echo start
  2. Serveur de calcul d'impôt >Début du service au client 1
  3. <--- Client 1 : aide
  4. ---> Client 1 : Commandes acceptées
  5. 1-aide
  6. 2-impot marié(O/N) nbEnfants salaireAnnuel
  7. 3-aurevoir
  • ligne 1 : l'écho du dialogue client / serveur est activé
  • ligne 2 : un client est arrivé
  • ligne 3 : il a envoyé la commande [aide]
  • lignes 4-7 : la réponse du serveur sur 4 lignes.

Arrêtons le service :

Cacher Sélectionnez
  1. stop
  2. L'erreur suivante s'est produite sur le serveur : Une opération de blocage a été interrompue par un appel à WSACancelBlockingCall
  3. Serveur de calcul d'impôt >
  • ligne 1 : on demande l'arrêt du service (pas de l'application elle-même)
  • ligne 2 : une exception due au fait que le serveur bloqué sur une attente de client a été brutalement interrompu à cause de la fermeture du service d'écoute.
  • ligne 3 : le service peut être maintenant relancé par start port ou arrêté par quit .

Avant que le service d'écoute ne soit arrêté, un client était servi sur une autre connexion. Cette connexion n'est pas fermée par la fermeture de la socket d'écoute. Le client peut continuer à émettre des commandes : le thread de service qui lui avait été associé avant la fermeture du service d'écoute, continue à lui répondre :

Image non disponible

XII-I-3. Le code du serveur Tcp de calcul d'impôt

Image non disponible
Image non disponible

Le code du serveur [ServeurImpot.cs] est le suivant :

Cacher Sélectionnez
  1. ...
  2. namespace Chap9 {
  3.     public class ServeurImpot {
  4.
  5.         // données partagées entre les threads et méthodes
  6.         private static IImpotMetier metier = null;
  7.         private static int port;
  8.         private static TcpListener service;
  9.         private static bool actif = false;
  10.         private static bool echo = false;
  11.
  12.         // programme principal
  13.         public static void Main(string[] args) {
  14.             // instanciations couches [metier] et [dao]
  15.             IApplicationContext ctx = null;
  16.             metier = null;
  17.             try {
  18.                 // contexte Spring
  19.                 ctx = ContextRegistry.GetContext();
  20.                 // on demande une référence sur la couche [metier]
  21.                 metier = (IImpotMetier)ctx.GetObject("metier");
  22.
  23.                 // configuration pool de threads
  24.                 ThreadPool.SetMinThreads(10, 10);
  25.                 ThreadPool.SetMaxThreads(10, 10);
  26.
  27.                 // lit les commandes d'administration du serveur tapées au clavier dans une boucle sans fin
  28.                 string commande = null;
  29.                 string[] champs = null;
  30.                 while (true) {
  31.                     // invite
  32.                     Console.Write("Serveur de calcul d'impôt >");
  33.                     // lecture commande
  34.                     commande = Console.ReadLine().Trim().ToLower();
  35.                     champs = Regex.Split(commande, @"\s+");
  36.                     // exécution commande
  37.                     switch (champs[0]) {
  38.                         case "start":
  39.                             // actif ?
  40.                             if (actif) {
  41.                                 //erreur
  42.                                 Console.WriteLine("Le serveur est déjà actif");
  43.                             } else {
  44.                                 // vérification port
  45.                                 if (champs.Length != 2 || !int.TryParse(champs[1], out port) || port <= 0) {
  46.                                     Console.WriteLine("Syntaxe : start port. Port incorrect");
  47.                                 } else {
  48.                                     // on lance le service d'écoute
  49.                                     ThreadPool.QueueUserWorkItem(doEcoute, null);
  50.                                 }
  51.                             }
  52.                             break;
  53.                         case "echo":
  54.                             // echo start / stop
  55.                             if (champs.Length != 2 || (champs[1] != "start" && champs[1] != "stop")) {
  56.                                 Console.WriteLine("Syntaxe : echo start / stop");
  57.                             } else {
  58.                                 echo = champs[1] == "start";
  59.                             }
  60.                             break;
  61.                         case "stop":
  62.                             // fin du service
  63.                             if (actif) {
  64.                                 service.Stop();
  65.                                 actif = false;
  66.                             }
  67.                             break;
  68.                         case "status":
  69.                             // état du serveur
  70.                             if (actif) {
  71.                                 Console.WriteLine("Le service est lancé sur le port {0}", port);
  72.                             } else {
  73.                                 Console.WriteLine("Le service n'est pas lancé}");
  74.                             }
  75.                             break;
  76.                         case "quit":
  77.                             // on quitte l'application
  78.                             Console.WriteLine("Fin du service");
  79.                             Environment.Exit(0);
  80.                             break;
  81.                         default:
  82.                             // commande incorrecte
  83.                             Console.WriteLine("Commande incorrecte. Utilisez (start,stop,echo, status, quit)");
  84.                             break;
  85.                     }
  86.                 }
  87.             } catch (Exception e1) {
  88.                 // affichage exception
  89.                 Console.WriteLine("L'erreur suivante s'est produite à l'initialisation de l'application : {0}", e1.Message);
  90.                 return;
  91.             }
  92.         }
  93.
  94.
  95.         private static void doEcoute(Object data) {
  96. ...
  97.         }
  98.
  99. ....
  100.     }
  101. }
  • lignes 18-21 : les couches [metier] et [dao] sont instanciées par Spring configuré par [App.config]. La variable globale metier de la ligne 6 est alors initialisée.
  • lignes 24-25 : on configure le pool de threads de l'application avec 10 threads minimum et maximum.
  • lignes 30-86 : la boucle de saisie des commandes d'administration du service (start, stop, quit, echo, status).
  • ligne 32 : invite du serveur pour chaque nouvelle commande
  • ligne 34 : lecture commande administrateur
  • ligne 35 : la commande est découpée en champs afin d'être analysée
  • lignes 38-52 : la commande start port qui a pour but de lancer le service d'écoute
  • ligne 40 : si le service est déjà actif, il n'y a rien à faire
  • ligne 45 : on vérifie que le port est bien présent et correct. Si oui, la variable globale port de la ligne 7 est positionnée.
  • ligne 49 : le service d'écoute va être géré par un thread secondaire afin que le thread principal puisse continuer à exécuter les commandes de la console. Si la méthode doEcoute réussit la connexion, les variables globales service de la ligne 8 et actif de la ligne 9 sont initialisées.
  • lignes 53-60 : la commande echo start / stop qui active / désactive l'écho du dialogue client / serveur sur la console
  • ligne 58 : la variable globale echo de la ligne 7 est positionnée
  • lignes 61-67 : la commande stop qui arrête le service d'écoute.
  • ligne 64 : arrêt du service d'écoute
  • lignes 68-75 : la commande status qui affiche l'état actif / inactif du service
  • lignes 76-80 : la commande quit qui arrête tout.

Le thread chargé d'écouter les demandes des clients exécute la méthode doEcoute suivante :

Cacher Sélectionnez
  1.         private static void doEcoute(Object data) {
  2.             // thread d'écoute des demandes des clients
  3.             try {
  4.                 // on crée le service
  5.                 service = new TcpListener(IPAddress.Any, port);
  6.                 // on le lance
  7.                 service.Start();
  8.                 // le serveur est actif
  9.                 actif = true;
  10.                 // suivi
  11.                 Console.WriteLine("Serveur de calcul d'impôt lancé sur le port {0}", port);
  12.                 // boucle de service aux clients
  13.                 TcpClient tcpClient = null;
  14.                 // n° client
  15.                 int numClient = 0;
  16.                 // boucle sans fin
  17.                 while (true) {
  18.                     // attente d'un client
  19.                     tcpClient = service.AcceptTcpClient();
  20.                     // le service est assuré par une autre tâche
  21.                     ThreadPool.QueueUserWorkItem(doService, new Client() { CanalTcp = tcpClient, NumClient = numClient });
  22.                     // client suivant
  23.                     numClient++;
  24.                 }
  25.             } catch (Exception ex) {
  26.                 // on signale l'erreur
  27.                 Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
  28.             }
  29.         }
  30.
  31.         // infos client
  32.         internal class Client {
  33.             public TcpClient CanalTcp { get; set; }        // liaison avec le client
  34.             public int NumClient { get; set; }            // n° de client
  35. }

On a là un code similaire à celui du serveur d'écho étudié au paragraphe , page . Nous ne commentons que ce qui est différent :

  • ligne 7 : le service d'écoute est lancé
  • ligne 9 : on note le fait que le service est désormais actif

Ligne 21, les clients sont servis par des threads de service exécutant la méthode doService suivante :

Cacher Sélectionnez
  1. private static void doService(Object infos) {
  2.             // on récupère le client qu'il faut servir
  3.             Client client = infos as Client;
  4.             // rend le service au client
  5.             Console.WriteLine("Début du service au client {0}", client.NumClient);
  6.             // exploitation liaison TcpClient
  7.             try {
  8.                 using (TcpClient tcpClient = client.CanalTcp) {
  9.                     using (NetworkStream networkStream = tcpClient.GetStream()) {
  10.                         using (StreamReader reader = new StreamReader(networkStream)) {
  11.                             using (StreamWriter writer = new StreamWriter(networkStream)) {
  12.                                 // flux de sortie non bufferisé
  13.                                 writer.AutoFlush = true;
  14.                                 // envoi d'un msg de bienvenue au client
  15.                                 writer.WriteLine("Bienvenue sur le serveur de calcul de l'impôt");
  16.                                 // boucle lecture demande/écriture réponse
  17.                                 string demande = null;
  18.                                 bool serviceFini = false;
  19.                                 while (!serviceFini && (demande = reader.ReadLine()) != null) {
  20.                                     // suivi console
  21.                                     if (echo) {
  22.                                         Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
  23.                                     }
  24.                                     // analyse demande
  25.                                     demande = demande.Trim().ToLower();
  26.                                     // demande vide ?
  27.                                     if (demande.Length == 0) {
  28.                                         // demande erronée
  29.                                         writeClient(writer,client.NumClient,"Commande non reconnue. Utilisez la commande aide.");
  30.                                         return;
  31.                                     }
  32.
  33.                                     // on décompose la demande en champs
  34.                                     string[] champs = Regex.Split(demande, @"\s+");
  35.                                     // analyse
  36.                                     switch (champs[0].ToLower()) {
  37.                                         case "aide":
  38.                                             writeClient(writer, client.NumClient, "Commandes acceptées\n1-aide\n2-impot marié(O/N) nbEnfants salaireAnnuel\n3-aurevoir");
  39.                                             break;
  40.                                         case "impot":
  41.                                             // on calcule l'impôt
  42.                                             writeClient(writer, client.NumClient, calculImpot(writer, client.NumClient, champs));
  43.                                             break;
  44.                                         case "aurevoir":
  45.                                             serviceFini = true;
  46.                                             writeClient(writer, client.NumClient, "Au revoir...");
  47.                                             break;
  48.                                         default:
  49.                                             writeClient(writer, client.NumClient, "Commande non reconnue. Utilisez la commande aide.");
  50.                                             break;
  51.                                     }
  52.                                 }
  53.                             }
  54.                         }
  55.                     }
  56.                 }
  57.             } catch (Exception e) {
  58.                 // erreur
  59.                 Console.WriteLine("L'erreur suivante s'est produite lors du service au client {0} : {1}", client.NumClient, e.Message);
  60.             } finally {
  61.                 Console.WriteLine("Fin du service au client {0}", client.NumClient);
  62.             }
  63.         }
  64.
  65.         private static void writeClient(StreamWriter writer, int numClient, string message) {
  66.             // echo console ?
  67.             if (echo) {
  68.                 Console.WriteLine("---> Client {0} : {1}", numClient, message);
  69.             }
  70.             // envoi msg au client
  71.             writer.WriteLine(message);
  72. }

De nouveau, on a là un code similaire à celui du serveur d'écho étudié au paragraphe , page . Nous ne commentons que ce qui est différent :

  • ligne 15 : une fois le client connecté, le serveur lui envoie un message de bienvenue.
  • lignes 19-52 : la boucle de lecture des commandes du client. La boucle s'arrête lorsque le client envoie la commande " aurevoir ".
  • ligne 27 : cas d'une commande vide
  • ligne 34 : la demande est décomposée en champs pour être analysée
  • ligne 37 : commande aide : le client demande la liste des commandes autorisées
  • ligne 40 : commande impot : le client demande un calcul d'impôt. On répond avec le message retourné par la méthode calculImpot que nous allons détailler prochainement.
  • ligne 44 : commande aurevoir : le client indique qu'il a terminé.
  • ligne 45 : on se prépare à sortir de la boucle de lecture des demandes du clients (lignes 19-52)
  • ligne 46 : on répond au client par un message d'au revoir
  • ligne 48 : une commande incorrecte. On envoie au client un message d'erreur.

Le traitement de la commande impot est assurée par la méthode calculImpot suivante :

Cacher Sélectionnez
  1. private static string calculImpot(StreamWriter writer, int numClient, string[] champs) {
  2.             // demande calcul marié(O/N) nbEnfants salaireAnnuel
  3.             // il faut 4 champs
  4.             if (champs.Length != 4) {
  5.                 return "Commande calcul incorrecte. Utilisez la commande aide.";
  6.             }
  7.             // champs [1]
  8.             string marié = champs[1];
  9.             if (marié != "o" && marié != "n") {
  10.                 return "Commande calcul incorrecte. Utilisez la commande aide.";
  11.             }
  12.             // champs [2]
  13.             int nbEnfants;
  14.             if (!int.TryParse(champs[2], out nbEnfants)) {
  15.                 return "Commande calcul incorrecte. Utilisez la commande aide.";
  16.             }
  17.             // champs [3]
  18.             int salaireAnnuel;
  19.             if (!int.TryParse(champs[3], out salaireAnnuel)) {
  20.                 return "Commande calcul incorrecte. Utilisez la commande aide.";
  21.             }
  22.             // c'est bon - on calcule l'impôt
  23.             int impot = 0;
  24.             try {
  25.                 impot = metier.CalculerImpot(marié == "o", nbEnfants, salaireAnnuel);
  26.                 return impot.ToString();
  27.             } catch (Exception ex) {
  28.                 return ex.Message;
  29.             }
  30.         }
  • ligne 1 : la méthode reçoit comme 3ième paramètre le tableau des champs de la commande impot . Si celle-ci a été correctement formulée, elle est de la forme impot marié nbEnfants salaireAnnuel . La méthode rend comme résultat la réponse à envoyer au client.
  • ligne 4 : on vérifie que la commande a 4 champs
  • ligne 8 : on vérifie que le champ marié est valide
  • ligne 14 : on vérifie que le champ nbEnfants est valide
  • ligne 19 : on vérifie que le champ salaireAnnuel est valide
  • ligne 25 : l'impôt est calculé à l'aide de la méthode CalculerImpot de la couche [metier]. On rappelle que cette couche est encapsulée dans une DLL.
  • ligne 26 : si la couche [metier] a rendu un résultat, celui-ci est rendu au client.
  • ligne 28 : si la couche [metier] a lancé une exception, le message de celle-ci est rendu au client.

XII-I-4. Le client graphique du serveur Tcp de calcul d'impôt

XII-I-4-a. Le projet Visual Studio

Image non disponible

Le projet Visual studio du client graphique sera le suivant :

Image non disponible
  • en [1], les deux projets de la solution, un pour chacune des deux couches de l'application
  • en [2], le client Tcp qui joue le rôle de couche [metier] pour la couche [ui]. Aussi utiliserons-nous les deux termes.
  • en [3], la couche [ui] de la version 7, à un détail près dont nous parlerons

XII-I-4-b. La couche [metier]

L'interface IImpotMetier n'a pas changé. C'est toujours celle de la version 7 :

Cacher Sélectionnez
  1. namespace Metier {
  2.     public interface IImpotMetier {
  3.         int CalculerImpot(bool marié, int nbEnfants, int salaire);
  4.     }
  5. }

L'implémentation de cette interface est la classe [ImpotMetierTcp] suivante :

Cacher Sélectionnez
  1. using System.Net.Sockets;
  2. using System.IO;
  3. namespace Metier {
  4.     public class ImpotMetierTcp : IImpotMetier {
  5.
  6.         // informations [serveur]
  7.         private string Serveur { get; set; }
  8.         private int Port { get; set; }
  9.
  10.         // calcul de l'impôt
  11.         public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
  12.                 // on se connecte au service
  13.                 using (TcpClient tcpClient = new TcpClient(Serveur, Port)) {
  14.                     using (NetworkStream networkStream = tcpClient.GetStream()) {
  15.                         using (StreamReader reader = new StreamReader(networkStream)) {
  16.                             using (StreamWriter writer = new StreamWriter(networkStream)) {
  17.                                 // flux de sortie non bufferisé
  18.                                 writer.AutoFlush = true;
  19.                                 // on saute le msg de bienvenue
  20.                                 reader.ReadLine();
  21.                                 // demande
  22.                                 writer.WriteLine(string.Format("impot {0} {1} {2}",marié ? "o" : "n",nbEnfants, salaire));
  23.                                 // réponse
  24.                                 return int.Parse(reader.ReadLine());
  25.                             }
  26.                         }
  27.                     }
  28.                 }
  29.             }
  30.         }
  31.     }
  • ligne 7 : le nom ou l'adresse Ip du serveur Tcp de calcul d'impôt
  • ligne 8 : le port d'écoute de ce serveur
  • ces deux propriétés seront initialisées par Spring lors de l'instanciation de la classe [ImpotMetierTcp].
  • ligne 11 : la méthode de calcul de l'impôt. Lorsqu'elle s'exécute, les propriétés Serveur et Port sont déjà initialisées. On retrouve dans le code la démarche classique d'un client Tcp
  • ligne 13 : la connexion avec le serveur est ouverte
  • lignes 14-16 : on récupère (ligne 14) le flux réseau associé à cette connexion duquel on tire un flux de lecture (ligne 15) et un flux d'écriture (ligne 16).
  • ligne 18 : le flux d'écriture doit être non bufferisé
  • ligne 20 : ici, il faut se rappeler qu'à l'ouverture de la connexion, le serveur envoie au client une 1re ligne qui est le message de bienvenue " Bienvenue sur le serveur de calcul de l'impôt ". Ce message est lu et ignoré.
  • ligne 22 : on envoie au serveur la commande du type : impot o 2 60000 pour lui demander de calculer l'impôt d'une personne mariée ayant 2 enfants et un salaire annuel de 60000 euros.
  • ligne 24 : le serveur répond par le montant de l'impôt sous la forme "4282" ou bien avec un message d'erreur si la commande était mal formée (ça n'arrivera pas ici) ou si le calcul de l'impôt a rencontré un problème. Ici, ce dernier cas n'est pas géré mais il aurait été certainement plus "propre" de le faire. En effet, si la ligne lue est un message d'erreur, une exception sera lancée parce que la conversion vers un entier va échouer. L'exception récupérée par l'interface graphique va être une erreur de conversion alors que l'exception originelle est d'une tout autre nature. Le lecteur est invité à améliorer ce code.
  • lignes 25-28 : libération de toutes les ressources utilisées avec une clause "using".

La couche [metier] est compilée dans la DLL ImpotsV8-metier.dll :

Image non disponible

XII-I-4-c. La couche [ui]

Image non disponible

La couche [ui] [1,3] est celle étudiée dans la version 7 au paragraphe , page à trois détails près :

  • la configuration de la couche [metier] dans [App.config] est différente parce que l'implémentation de celle-ci a changé
  • l'interface graphique [Form1.cs] a été modifiée pour afficher une éventuelle exception
  • la couche [metier] est dans la DLL [ImpotsV8-metier.dll].

Le fichier [App.config] est le suivant :

Cacher Sélectionnez
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.
  4.     <configSections>
  5.         <sectionGroup name="spring">
  6.             <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
  7.             <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
  8.         </sectionGroup>
  9.     </configSections>
  10.
  11.     <spring>
  12.         <context>
  13.             <resource uri="config://spring/objects" />
  14.         </context>
  15.         <objects xmlns="http://www.springframework.net">
  16.             <object name="metier" type="Metier.ImpotMetierTcp, ImpotsV8-metier">
  17.                 <property name="Serveur" value="localhost"/>
  18.                 <property name="Port" value="27"/>
  19.             </object>
  20.         </objects>
  21.     </spring>
  22. </configuration>
  • ligne 16 : instanciation de la couche [metier] avec la classe Metier.ImpotMetierTcp de la DLL ImpotsV8-metier.dll
  • lignes 17-18 : les propriétés Serveur et Port de la classe Metier.ImpotMetierTcp sont initialisées. Le serveur sera sur la machine localhost et opèrera sur le port 27.

L'interface graphique présentée à l'utilisateur est la suivante :

Image non disponible
  • en [1], on a ajouté un TextBox pour afficher une éventuelle exception. Ce champ n'existait pas dans la version précédente.

A part ce détail, le code du formulaire est celui déjà étudié au paragraphe , page . Le lecteur est invité à s'y reporter. En [2], on voit un exemple d'exécution obtenu avec un serveur lancé de la façon suivante :

Cacher Sélectionnez
  1. Serveur de calcul d'impôt >start 27
  2. Serveur de calcul d'impôt lancé sur le port 27
  3. Serveur de calcul d'impôt >echo start
  4. Serveur de calcul d'impôt >
  5. ...
  6. Début du service au client 9
  7. <--- Client 9 : impot o 2 60000
  8. ---> Client 9 : 4282
  9. Fin du service au client 9

La copie d'écran [2] du client correspond aux lignes du client 9 ci-dessus.

XII-I-5. Conclusion

De nouveau, nous avons pu réutiliser du code existant, sans modifications (couches [metier], [dao] du serveur) ou avec très peu de modifications (couche [ui] du client). Cela a été rendu possible par notre utilisation systématique d'interfaces et l'instanciation de celles-ci avec Spring. Si dans la version 7, nous avions mis le code métier directement dans les gestionnaires d'événements de l'interface graphique, ce code métier n'aurait pas été réutilisable. C'est l'inconvénient majeur des architectures 1 couche.

On notera enfin que la couche [ui] n'a aucune connaissance du fait que c'est un serveur distant qui lui calcule le montant de l'impôt.


CSharp tutoriel 2008

Apprentissage du langage C#

  • XIII. Services Web
    • XIII-A. Introduction
    • XIII-B. Un premier service Web avec Visual Web Developer
      • XIII-B-1. La partie serveur
      • XIII-B-2. La partie client
    • XIII-C. Un service Web d'opérations arithmétiques
      • XIII-C-1. La partie serveur
      • XIII-C-2. La partie client
    • XIII-D. Un service web de calcul d'impôt
      • XIII-D-1. La partie serveur
      • XIII-D-2. Un client graphique windows pour le service web distant
        • XIII-D-2-a. La nouvelle couche [metier]
        • XIII-D-2-b. La nouvelle couche [ui]
    • XIII-E. Un client web pour le service web de calcul d'impôt
    • XIII-F. Un client console Java pour le service web de calcul d'impôt

XIII. Services Web

XIII-A. Introduction

Nous avons présenté dans le chapitre précédent plusieurs applications client-serveur Tcp-Ip. Dans la mesure où les clients et le serveur échangent des lignes de texte, ils peuvent être écrits en n'importe quel langage. Le client doit simplement connaître le protocole de dialogue attendu par le serveur.

Les services Web sont également des applications serveur Tcp-Ip. Ils présentent les caractéristiques suivantes :

  • Ils sont hébergés par des serveurs web et le protocole d'échanges client-serveur est HTTP (HyperText Transport Protocol), un protocole au-dessus de TCP-IP.
  • Le service Web a un protocole de dialogue standard quelque soit le service assuré. Un service Web offre divers services S1, S2, .., Sn. Chacun d'eux attend des paramètres fournis par le client et rend à celui-ci un résultat. Pour chaque service, le client a besoin de savoir :
    • le nom exact du service Si
    • la liste des paramètres qu'il faut lui fournir et leur type
    • le type de résultat retourné par le service

Une fois, ces éléments connus, le dialogue client-serveur suit le même format, quelque soit le service web interrogé. L'écriture des clients est ainsi normalisée.

  • Pour des raisons de sécurité vis à vis des attaques venant de l'internet, beaucoup d'organisations ont des réseaux privés et n'ouvrent sur Internet que certains ports de leurs serveurs : essentiellement le port 80 du service web. Tous les autres ports sont verrouillés. Aussi les applications client-serveur telles que présentées dans le chapitre précédent sont-elles construites au sein du réseau privé (intranet) et ne sont en général pas accessibles de l'extérieur. Loger un service au sein d'un serveur web le rend accessible à toute la communauté internet.
  • Le service Web peut être modélisé comme un objet distant. Les services offerts deviennent alors des méthodes de cet objet. Un client peut avoir accès à cet objet distant comme s'il était local. Cela cache toute la partie communication réseau et permet de construire un client indépendant de cette couche. Si celle-ci vient à changer, le client n'a pas à être modifié.
  • Comme pour les applications client-serveur Tcp-Ip présentées dans le chapitre précédent, le client et le serveur peuvent être écrits dans un langage quelconque. Ils échangent des lignes de texte. Celles-ci comportent deux parties :
    • les entêtes nécessaires au protocole HTTP
    • le corps du message. Pour une réponse du serveur au client, celui-ci est au format XML (eXtensible Markup Language). Pour une demande du client au serveur, le corps du message peut avoir plusieurs formes dont XML. La demande XML du client peut avoir un format particulier appelé SOAP (Simple Object Access Protocol). Dans ce cas, la réponse du serveur suit aussi le format SOAP.

L'architecture d'une application client / serveur à base d'un service web est la suivante :

Image non disponible

C'est une extension de l'architecture 3 couches à laquelle on ajoute des classes de communication réseau spécialisées. On a déjà rencontré une architecture similaire avec l'application client graphique windows / serveur Tcp d'impôts au paragraphe , page .

Explicitons ces généralités avec un premier exemple.

XIII-B. Un premier service Web avec Visual Web Developer

Nous allons construire une première application client / serveur ayant l'architecture simplifiée suivante :

Image non disponible

XIII-B-1. La partie serveur

Nous avons indiqué qu'un service web était hébergé par un serveur web. L'écriture d'un service web entre dans le cadre général de la programmation web, côté serveur. Nous avons eu précédemment l'occasion d'écrire des clients web, ce qui est aussi de la programmation web mais côté client cette fois. Le terme programmation web désigne le plus souvent la programmation côté serveur plutôt que celle côté client. Pour développer des services web ou plus généralement des applications web, Visual C# n'est pas l'outil approprié. Nous allons utiliser Visual Developer l'une des versions Express de Visual Studio 2008 téléchargeables [2] à l'adresse [1] : [http://msdn.microsoft.com/fr-fr/express/future/bb421473(en-us).aspx] (mai 2008) :

Image non disponible
  • [1] : l'adresse de téléchargement
  • [2] : l'onglet des téléchargements
  • [3] : télécharger Visual Developer 2008

Pour créer un premier service web, on pourra procéder comme suit, après avoir lancé Visual Developer :

Image non disponible
  • [1] : prendre l'option File / New Web Site
  • [2] : choisir une application de type ASP.NET Web Service
  • [3] : choisir le langage de développement : C#
  • [4] : indiquer le dossier où créer le projet
Image non disponible
  • [5] : le projet créé dans Visual Web Developer
  • [6] : le dossier du projet sur le disque

Une application web est structurée de la façon suivante dans Web Developer :

  • une racine dans laquelle on trouve les documents du site web (pages web statiques Html, images, pages web dynamiques .aspx, services web .asmx…). On y trouve également le fichier [web.config] qui est le fichier de configuration de l'application web. Il joue le même rôle que le fichier [App.config] des applications windows et est structuré de la même façon.
  • un dossier [App_Code] dans lequel on trouve les classes et interfaces du site web destinées à être compilées.
  • un dossier [App_Data] dans lequel on mettra des données exploitées par les classes de [App_Code]. On pourra par exemple y trouver une base SQL Server *.mdf.

[Service.asmx] est le service web dont nous avons demandé la création. Il ne contient que la ligne suivante :

Cacher Sélectionnez
  1. <%@ WebService Language="C#" CodeBehind="~/App_Code/Service.cs" Class="Service" %>

Le code-source ci-dessus est destiné au serveur Web qui hébergera l'application. En mode production, ce serveur est en général IIS (Internet Information Server), le serveur web de Microsoft. Visual Web Developer embarque un serveur web léger qui est utilisé en mode développement. La directive précédente indique au serveur web :

  • [Service.asmx] est un service Web (directive WebService )
  • écrit en C# (attribut Language )
  • que le code C# du service web se trouve dans le fichier [~/App_Code/Service.cs] (attribut CodeBehind ). C'est là qu'ira le chercher le serveur web pour le compiler.
  • que la classe implémentant le service web s'appelle Service (attribut Class )

Le code C# [Service.cs] du service web généré par Visual Developer est le suivant :

Cacher Sélectionnez
  1. using System.Web.Services;
  2.
  3. [WebService(Namespace = "http://tempuri.org/")]
  4. [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
  5. // To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line.
  6. // [System.Web.Script.Services.ScriptService]
  7. public class Service : System.Web.Services.WebService
  8. {
  9.     public Service () {
  10.
  11.         //Uncomment the following line if using designed components
  12.         //InitializeComponent();
  13.     }
  14.
  15.     [WebMethod]
  16.     public string HelloWorld() {
  17.         return "Hello World";
  18.     }
  19.
  20. }

La classe Service ressemble à une classe C# classique avec cependant quelques points à noter :

  • ligne 7 : la classe dérive de la classe WebService définie dans l'espace de noms System.Web.Services . Cet héritage n'est pas toujours obligatoire. Dans cet exemple notamment on pourrait s'en passer.
  • ligne 3 : la classe elle-même est précédée d'un attribut [WebService(Namespace="http://tempuri.org/")] destiné à donner un espace de noms au service web. Un vendeur de classes donne un espace de noms à ses classes afin de leur donner un nom unique et éviter ainsi des conflits avec des classes d'autres vendeurs qui pourraient porter le même nom. Pour les services Web, c'est pareil. Chaque service web doit pouvoir être identifié par un nom unique, ici par http://tempuri.org/. Ce nom peut être quelconque. Il n'a pas forcément la forme d'une Uri Http.
  • ligne 15 : la méthode HelloWorld est précédée d'un attribut [WebMethod] qui indique au compilateur que la méthode doit être rendue visible aux clients distants du service web. Une méthode non précédée de cet attribut n'est pas visible par les clients du service web. Ce pourrait être une méthode interne utilisée par d'autres méthodes mais pas destinée à être publiée.
  • ligne 9 : le constructeur du service web. Il est inutile dans notre application.

La classe [Service.cs] générée est transformée de la façon suivante :

Cacher Sélectionnez
  1. using System.Web.Services;
  2.
  3. [WebService(Namespace = "http://st.istia.univ-angers.fr")]
  4. [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
  5. public class Service : System.Web.Services.WebService
  6. {
  7.     [WebMethod]
  8.     public string DisBonjourALaDame(string nomDeLaDame) {
  9.         return string.Format("Bonjour Mme {0}", nomDeLaDame);
  10.     }
  11.
  12. }

Le fichier de configuration [web.config] généré pour l'application web est le suivant :

Cacher Sélectionnez
  1. <?xml version="1.0"?>
  2. <!--
  3.     Note: As an alternative to hand editing this file you can use the
  4.     web admin tool to configure settings for your application. Use
  5.     the Website->Asp.Net Configuration option in Visual Studio.
  6.     A full list of settings and comments can be found in
  7.     machine.config.comments usually located in
  8.     \Windows\Microsoft.Net\Framework\v2.x\Config
  9. -->
  10. <configuration>
  11.
  12.
  13.     <configSections>
  14.       <sectionGroup name="system.web.extensions" type="System.Web.Configuration.SystemWebExtensionsSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
  15. ...
  16.       </sectionGroup>
  17.     </configSections>
  18.
  19.
  20.     <appSettings/>
  21.     <connectionStrings/>
  22. ...
  23. </configuration>

Le fichier fait 140 lignes. Il est complexe et nous ne le commenterons pas. Nous le garderons tel quel. Ci-dessus, on retrouve les balises < configuration >, < configSections >, < sectionGroup >, < appSettings >, < connectionString > que nous avons rencontrées dans le fichier [App.config] des applications windows.

Nous avons un service web opérationnel qui peut être exécuté :

Image non disponible
  • [1,2] : on clique droit sur [Service.asmx] et on demande à voir la page dans un navigateur
  • [3] : Visual Web Developer lance son serveur web intégré et met l'icône de celui-ci en bas à droite dans la barre des tâches. Le serveur web est lancé sur un port aléatoire, ici 1906. L'Uri affichée /WsHello est le nom du site web [4].

Visual Web Developer a également lancé un navigateur pour afficher la page demandée à savoir [Service.asmx] :

Image non disponible
  • en [1], l'Uri de la page. On retrouve l'Uri du site [http://localhost:1906/WsHello] suivi de celle de la page /Service.asmx.
  • en [2], le suffixe .asmx a indiqué au serveur web qu'il s'agissait non pas d'une page web normale (suffixe .aspx) donnant naissance à une page Html, mais de la page d'un service web. Il génère alors de façon automatique une page web présentant un lien pour chacune des méthodes du service web ayant l'attribut [WebMethod]. Ces liens sont des liens permettant de tester les méthodes.

Un clic sur le lien [2] ci-dessus nous amène à la page suivante :

Image non disponible
  • en [1], on notera l'Uri [http://localhost:1906/WsHello/Service.asmx?op=DisBonjourALaDame] de la nouvelle page. C'est l'Uri du service web avec un paramètre op=M , où M est le nom d'une des méthodes du service web.
  • Rappelons la signature de la méthode [DisBonjourALaDame] :
Cacher Sélectionnez
  1.     public string DisBonjourALaDame(string nomDeLaDame) ;

La méthode admet un paramètre de type string et rend un résultat de type string également. La page nous permet d'exécuter la méthode [DisBonjourALaDame] : en [2] on met la valeur du paramètre nomDeLaDame et en [3], on demande l'exécution de la méthode. Nous obtenons le résultat suivant :

Image non disponible
  • en [1], on notera que l'Uri de la réponse n'est pas identique à celle de la demande. Elle a changé.
  • en [2], la réponse du serveur web. On notera les points suivants :
  • c'est une réponse XML et non HTML
  • le résultat de la méthode [DisBonjourALaDame] est encapsulé dans une balise <string> représentant son type.
  • la balise <string> a un attribut xmlns ( xml name space ) qui est l'espace de noms que nous avons donné à notre service web (ligne 1 ci-dessous).
Cacher Sélectionnez
  1. [WebService(Namespace = "http://st.istia.univ-angers.fr")]
  2. [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
  3. public class Service : System.Web.Services.WebService

Pour savoir comment le navigateur web a fait sa demande, il faut regarder le code Html du formulaire de test :

Cacher Sélectionnez
  1. ...
  2. <span>
  3.   <p class="intro">Click <a href="Service.asmx">here</a> for a complete list of operations.</p>
  4.   <h2>DisBonjourALaDame</h2>
  5.   <p class="intro"></p>
  6.
  7.   <h3>Test</h3>
  8.
  9.          To test the operation using the HTTP POST protocol, click the 'Invoke' button.
  10.
  11.    <form action='http://localhost:1906/WsHello/Service.asmx/DisBonjourALaDame' method="POST">
  12.       <table>
  13.               <tr>
  14.                     <td>Parameter</td>
  15.                     <td>Value</td>
  16.                 </tr>
  17.                 <tr>
  18.                <td>nomDeLaDame:</td>
  19.                <td><input type="text" size="50" name="nomDeLaDame"></td>
  20.            </tr>
  21.                 <tr>
  22.                    <td></td>
  23.                   <td align="right"> <input type="submit" value="Invoke" class="button"></td>
  24.            </tr>
  25.       </table>
  26.     </form>
  27. <span>
  28. ...
  • ligne 11 : les valeurs du formulaire (balise form) seront postées (attribut method) à l'Url [ http://localhost:1906/WsHello/Service.asmx/DisBonjourALaDame] (attribut action).
  • ligne 19 : le champ de saisie s'appelle nomDeLaDame (attribut name).

Demander l'exécution du service web [/Service.asmx] nous a permis de tester ses méthodes et d'avoir un minimum de compréhension des échanges client serveur.

XIII-B-2. La partie client

Image non disponible

Il est possible d'implémenter le client du service web distant ci-dessus avec un client Tcp-Ip basique. Voici par exemple le dialogue client / serveur réalisé avec un client putty connecté au service web distant (localhost,1906) :

Cacher Sélectionnez
  1. POST /WsHello/Service.asmx/DisBonjourALaDame HTTP/1.1
  2. Host: localhost
  3. Content-Type: application/x-www-form-urlencoded
  4. Content-Length: 23
  5.
  6. HTTP/1.1 100 Continue
  7. Server: ASP.NET Development Server/9.0.0.0
  8. Date: Sat, 10 May 2008 08:36:41 GMT
  9. Content-Length: 0
  10.
  11. nomDeLaDame=Carla+Bruni
  12. HTTP/1.1 200 OK
  13. Server: ASP.NET Development Server/9.0.0.0
  14. Date: Sat, 10 May 2008 08:36:47 GMT
  15. X-AspNet-Version: 2.0.50727
  16. Cache-Control: private, max-age=0
  17. Content-Type: text/xml; charset=utf-8
  18. Content-Length: 119
  19. Connection: Close
  20.
  21. <?xml version="1.0" encoding="utf-8"?>
  22. <string xmlns="http://st.istia.univ-angers.fr">Bonjour Mme Carla Bruni</string>
  • lignes 1-5 : messages envoyés par le client putty
  • ligne 1 : commande POST
  • lignes 6-10 : réponse du serveur. Elle signifie que le client peut envoyer les valeurs du POST.
  • ligne 11 : les valeurs postées sous la forme param1=val1&param2=val2& …. Certains caractères doivent être en caractères acceptables dans une Url. C'est ce qu'on a appelé précédemment une Url encodée. Ici le formulaire n'a qu'un unique paramètre nommé nomDeLaDame . La valeur postée a au total 23 caractères. Cette taille doit être déclarée dans l'entête Http de la ligne 4.
  • lignes 12-22 : la réponse du serveur
  • ligne 22 : le résultat de la méthode web [DisBonjourALaDame].

Avec Visual C#, il est possible de générer à l'aide d'un assistant le client d'un service web distant. C'est ce que nous voyons maintenant.

Image non disponible

La couche [1] ci-dessus est implémentée par un projet Visual studio C# de type Application Windows et nommé ClientWsHello :

Image non disponible
  • en [1], le projet ClientWsHello dans Visual C#
  • en [2], l'espace de noms par défaut du projet sera Client (clic droit sur le projet / Properties / Application). Cet espace de noms va servir à construire l'espace de noms du client qui va être généré.
  • en [3], clic droit sur le projet pour lui ajouter une référence à un service web distant
Image non disponible
  • en [4], mettre l'Uri du service web construit précédemment
  • en [4b], connecter Visual C# au service web désigné par [4]. Visual C# va récupérer la description du service web et grâce à cette description va pouvoir en générer un client.
  • en [5], une fois la description du service web récupérée, Visual C# peut en afficher les méthodes publiques
  • en [6], indiquer un espace de noms pour le client qui va être généré. Celui-ci sera ajouté à l'espace de noms défini en [2]. Ainsi l'espace de noms du client sera Client.WsHello .
  • en [6b] valider l'assistant.
  • en [7], la référence au service web WsHello apparaît dans le projet. Par ailleurs, un fichier de configuration [app.config] a été créé.
  • en [8], visualiser tous les fichiers du projet.
  • en [9], la référence au service web WsHello contient divers fichiers que nous n'expliciterons pas. Nous jetterons cependant un coup d'œil sur le fichier [Reference.cs] qui est le code C# du client généré :
Cacher Sélectionnez
  1. namespace Client.WsHello {
  2. ...
  3.     public partial class ServiceSoapClient : System.ServiceModel.ClientBase<Client.WsHello.ServiceSoap>, Client.WsHello.ServiceSoap {
  4.
  5.         public ServiceSoapClient() {
  6.         }
  7. ...
  8.         public string DisBonjourALaDame(string nomDeLaDame) {
  9.             Client.WsHello.DisBonjourALaDameRequest inValue = new Client.WsHello.DisBonjourALaDameRequest();
  10.             inValue.Body = new Client.WsHello.DisBonjourALaDameRequestBody();
  11.             inValue.Body.nomDeLaDame = nomDeLaDame;
  12.             Client.WsHello.DisBonjourALaDameResponse retVal = ((Client.WsHello.ServiceSoap)(this)).DisBonjourALaDame(inValue);
  13.             return retVal.Body.DisBonjourALaDameResult;
  14.         }
  15.     }
  16. }
  • ligne 1 : l'espace de nom du client généré est Client.WsHello . Si on souhaite changer cet espace de noms, c'est là qu'il faut le faire.
  • ligne 3 : la classe ServiceSoapClient est la classe du client généré. C'est une classe proxy dans le sens où elle va cacher à l'application windows le fait qu'un service web distant est utilisé. L'application windows va utiliser la classe WsHello distante via la classe locale Client.WsHello.ServiceSoapClient . Pour créer une instance du client, on utilisera le constructeur de la ligne 5 :
Cacher Sélectionnez
  1. Client.WsHello.ServiceSoapClient client=new Client.WsHello.ServiceSoapClient();
  • ligne 8 : la méthode DisBonjourALaDame est le pendant, côté client, de la méthode DisBonjourALaDame du service web. L'application windows utilisera la méthode distante DisBonjourALaDame via la méthode locale Client.WsHello.ServiceSoapClient.DisBonjourALaDame sous la forme suivante :
Cacher Sélectionnez
  1. string bonjour=client.DisBonjourALaDame("Carla Bruni");

Le fichier [app.config] généré est le suivant :

Cacher Sélectionnez
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.     <system.serviceModel>
  4.         <bindings>
  5.          ....
  6.         </bindings>
  7.         <client>
  8.             <endpoint address="http://localhost:1906/WsHello/Service.asmx"... />
  9.         </client>
  10.     </system.serviceModel>
  11. </configuration>

De ce fichier, nous ne retiendrons que la ligne 8 qui contient l'Uri du service web. Si celui-ci change d'Uri, le client windows n'a pas à être reconstruit. Il suffit de changer l'Uri du fichier [app.config].

Revenons à l'architecture de l'application windows que nous voulons construire :

Image non disponible

Nous avons construit la couche [client] du service web. La couche [ui] sera la suivante :

Image non disponible
n° | type | nom | rôle
1 | TextBox | textBoxNomDame | nom de la dame
2 | Button | buttonSalutations | pour se connecter au service web WsHello distant et interroger la méthode DisBonjourALaDame .
3 | Label | labelBonjour | le résultat renvoyé par le service web

Le code du formulaire [Form1.cs] est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Windows.Forms;
  3. using Client.WsHello;
  4.
  5. namespace ClientSalutations {
  6.     public partial class Form1 : Form {
  7.         public Form1() {
  8.             InitializeComponent();
  9.         }
  10.
  11.         private void buttonSalutations_Click(object sender, EventArgs e) {
  12.             // sablier
  13.             Cursor=Cursors.WaitCursor;
  14.             // interrogation service web
  15.             labelBonjour.Text = new ServiceSoapClient().DisBonjourALaDame(textBoxNomDame.Text.Trim());
  16.             // curseur normal
  17.             Cursor = Cursors.Arrow;
  18.         }
  19.     }
  20. }
  • ligne 15 : le client du service web est instancié. Il est de type Client.WsHello.ServiceSoapClient . L'espace de noms Client.WsHello est déclaré ligne 3. La méthode locale ServiceSoapClient().DisBonjourALaDame est appelée. On sait qu'elle même interroge la méthode distante de même nom du service web.

XIII-C. Un service Web d'opérations arithmétiques

Nous allons construire une seconde application client / serveur ayant de nouveau l'architecture simplifiée suivante :

Image non disponible

Le service web précédent offrait une unique méthode. Nous considérons un service Web qui offrira les 4 opérations arithmétiques :

  • ajouter(a,b) qui rendra a+b
  • soustraire(a,b) qui rendra a-b
  • multiplier(a,b) qui rendra a*b
  • diviser(a,b) qui rendra a/b

et qui sera interrogé par l'interface graphique suivante :

Image non disponible
  • en [1], l'opération à faire
  • en [2,3] : les opérandes
  • en [4], le bouton d'appel du service web
  • en [5], le résultat rendu par le service web

XIII-C-1. La partie serveur

Nous construisons un projet de type service web avec Visual Web Developer :

Image non disponible
  • en [1], l'application web WsOperations générée
  • en [2], l'application web WsOperations relookée de la façon suivante :
  • la page web [Service.asmx] a été renommée [Operations.asmx]
  • la classe [Service.cs] a été renommée [Operations.cs]
  • le fichier [web.config] a été supprimé afin de montrer qu'il n'est pas indispensable.

La page web [Service.asmx] contient la ligne suivante :

<% @ WebService Language ="C#" CodeBehind ="~/App_Code/Operations.cs" Class ="Operations" %>

Le service web est assuré par classe [Operations.cs] suivante :

Cacher Sélectionnez
  1. using System.Web.Services;
  2.
  3. [WebService(Namespace = "http://st.istia.univ-angers.fr/")]
  4. [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
  5. public class Operations : System.Web.Services.WebService
  6. {
  7.     [WebMethod]
  8.     public double Ajouter(double a, double b)
  9.     {
  10.         return a + b;
  11.     }
  12.
  13.     [WebMethod]
  14.     public double Soustraire(double a, double b)
  15.     {
  16.         return a - b;
  17.     }
  18.
  19.     [WebMethod]
  20.     public double Multiplier(double a, double b)
  21.     {
  22.         return a * b;
  23.     }
  24.
  25.     [WebMethod]
  26.     public double Diviser(double a, double b)
  27.     {
  28.         return a / b;
  29.     }
  30.
  31. }

Pour mettre en ligne le service web, nous procédons comme indiqué en [3]. Nous obtenons alors la page de test des 4 méthodes du service web WsOperations :

Image non disponible

Le lecteur est invité à tester les 4 méthodes.

XIII-C-2. La partie client

Image non disponible

Avec Visual C# nous créons une application Windows ClientWsOperations :

Image non disponible
  • en [1], le projet ClientWsOperations dans Visual C#
  • en [2], l'espace de noms par défaut du projet sera Client (clic droit sur le projet / Properties / Application). Cet espace de noms va servir à construire l'espace de noms du client qui va être généré.
  • en [3], clic droit sur le projet pour lui ajouter une référence à un service web existant
Image non disponible
  • en [4], mettre l'Uri du service web construit précédemment. Il faut pour cela regarder ce qui est affiché dans le champ adresse du navigateur qui affiche la page de test du service web.
  • en [4b], connecter Visual C# au service web désigné par [4]. Visual C# va récupérer la description du service web et grâce à cette description va pouvoir en générer un client.
  • en [5], une fois la description du service web récupérée, Visual C# peut en afficher les méthodes publiques
  • en [6], indiquer un espace de noms pour le client qui va être généré. Celui-ci sera ajouté à l'espace de noms défini en [2]. Ainsi l'espace de noms du client sera Client.WsOperations .
  • en [6b] valider l'assistant.
  • en [7], la référence au service web WsOperations apparaît dans le projet. Par ailleurs, un fichier de configuration [app.config] a été créé.

On rappelle que le client généré est de type Client.WsOperations.OperationsSoapClient où

  • Client.WsOperations est l'espace de noms du client du service web
  • Operations est la classe du service web distant.

Même s'il y a une façon logique de construire ce nom, il est souvent plus simple de le retrouver dans le fichier [Reference.cs] qui est un fichier caché par défaut. Son contenu est le suivant :

Cacher Sélectionnez
  1. namespace Client.WsOperations {
  2.  ...
  3.     public partial class OperationsSoapClient : System.ServiceModel.ClientBase<Client.WsOperations.OperationsSoap>, Client.WsOperations.OperationsSoap {
  4.
  5.         public OperationsSoapClient() {
  6.         }
  7. ...
  8.         public double Ajouter(double a, double b) {
  9.             ...
  10.         }
  11.
  12.         public double Soustraire(double a, double b) {
  13.             ...
  14.         }
  15.
  16.         public double Multiplier(double a, double b) {
  17.             ...
  18.         }
  19.
  20.         public double Diviser(double a, double b) {
  21.             ...
  22.         }
  23.     }
  24. }

Les méthodes Ajouter , Soustraire , Multiplier , Diviser du service web distant seront accédées via les méthodes proxy de même nom (lignes 8, 12, 16, 20) du client de type Client.WsOperations.OperationsSoapClient (ligne 3).

Il nous reste à construire l'interface graphique :

Image non disponible
n° | type | nom | rôle
1 | ComboBox | comboBoxOperations | liste des opérations arithmétiques
2 | TextBox | textBoxA | nombre a
3 | TextBox | textBoxB | nombre b
4 | Button | buttonExécuter | interroge le service web distant
5 | Label | labelRésultat | le résultat de l'opération

Le code de [Form1.cs] est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using System.Windows.Forms;
  3. using Client.WsOperations;
  4.
  5. namespace ClientWsOperations {
  6.     public partial class Form1 : Form {
  7.         // tableau des opérations
  8.         private string[] opérations = { "Ajouter", "Soustraire", "Multiplier", "Diviser" };
  9.         // service web à contacter
  10.         private OperationsSoapClient opérateur = new OperationsSoapClient();
  11.
  12.         // constructeur
  13.         public Form1() {
  14.             InitializeComponent();
  15.         }
  16.
  17.         private void Form1_Load(object sender, EventArgs e) {
  18.             // remplissage combo des opérations
  19.             comboBoxOperations.Items.AddRange(opérations);
  20.             comboBoxOperations.SelectedIndex = 0;
  21.         }
  22.
  23.         private void buttonExécuter_Click(object sender, EventArgs e) {
  24.             // vérification des paramètres a et b de l'opération
  25.             textBoxMessage.Text = "";
  26.             bool erreur = false;
  27.             Double a = 0;
  28.             if (!Double.TryParse(textBoxA.Text, out a)) {
  29.                 textBoxMessage.Text += "Nombre a erroné...";
  30.             }
  31.             Double b = 0;
  32.             if (!Double.TryParse(textBoxB.Text, out b)) {
  33.                 textBoxMessage.Text += String.Format("{0}Nombre b erroné...", Environment.NewLine);
  34.             }
  35.             if (erreur) {
  36.                 return;
  37.             }
  38.             // exécution de l'opération
  39.             Double c=0;
  40.             try {
  41.                 switch (comboBoxOperations.SelectedItem.ToString()) {
  42.                     case "Ajouter":
  43.                         c=opérateur.Ajouter(a, b);
  44.                         break;
  45.                     case "Soustraire":
  46.                         c=opérateur.Soustraire(a, b);
  47.                         break;
  48.                     case "Multiplier":
  49.                         c=opérateur.Multiplier(a, b);
  50.                         break;
  51.                     case "Diviser":
  52.                         c=opérateur.Diviser(a, b);
  53.                         break;
  54.                 }
  55.                 // affichage résultat
  56.                 labelRésultat.Text = c.ToString();
  57.             } catch (Exception ex) {
  58.                 textBoxMessage.Text = ex.Message;
  59.             }
  60.         }
  61.     }
  62. }
  • ligne 3 : l'espace de noms du client du service web distant
  • ligne 10 : le client du service web distant est instancié en même temps que le formulaire
  • lignes 17-21 : le combo des opérations est rempli lors du chargement initial du formulaire
  • ligne 23 : exécution de l'opération demandée par l'utilisateur
  • lignes 25-37 : on vérifie que les saisies a et b sont bien des nombres réels
  • lignes 41-54 : un switch pour exécuter l'opération distante demandée par l'utilisateur
  • lignes 43, 46, 49, 52 : c'est le client local qui est interrogé. De façon transparente, ce dernier interroge le service web distant.

XIII-D. Un service web de calcul d'impôt

Nous reprenons l'application de calcul d'impôt maintenant bien connue. La dernière fois que nous avons travaillé avec, nous en avions fait un serveur Tcp distant qu'on pouvait appeler sur l'internet. Nous en faisons maintenant un service web.

L'architecture de la version 8 était la suivante :

Image non disponible

L'architecture de la version 9 sera similaire :

Image non disponible

C'est une architecture similaire à celle de la version 8 étudiée au paragraphe , page mais où le serveur et le client Tcp sont remplacés par un service web et son client proxy. Nous allons reprendre intégralement les couches [ui], [metier] et [dao] de la version 8.

XIII-D-1. La partie serveur

Nous construisons un projet de type service web avec Visual Web Developer :

Image non disponible
  • en [1], l'application web WsImpot générée
  • en [2], l'application web WsImpot relookée de la façon suivante :
  • la page web [Service.asmx] a été renommée [ServiceImpot.asmx]
  • la classe [Service.cs] a été renommée [ServiceImpot .cs]

La page web [ServiceImpot.asmx] contient la ligne suivante :

Cacher Sélectionnez
  1. <%@ WebService Language="C#" CodeBehind="~/App_Code/ServiceImpot.cs" Class="ServiceImpot" %>

Le service web est assuré par classe [ServiceImpot.cs] suivante :

Cacher Sélectionnez
  1. using System.Web.Services;
  2.
  3. [WebService(Namespace = "http://st.istia.univ-angers.fr/")]
  4. [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
  5. public class ServiceImpot : System.Web.Services.WebService
  6. {
  7.
  8.     [WebMethod]
  9.     public int CalculerImpot(bool marié, int nbEnfants, int salaire)
  10.     {
  11.         return 0;
  12.     }
  13.
  14. }

Le service web n'exposera que la méthode CalculerImpot de la ligne 9.

Revenons à l'architecture client / serveur de la version 8 :

Image non disponible

Le projet Visual studio du serveur [1] était le suivant :

Image non disponible
  • en [1], le projet. On y trouvait les éléments suivants :
  • [ServeurImpot.cs] : le serveur Tcp/Ip de calcul de l'impôt sous la forme d'une application console.
  • [dbimpots.sdf] : la base de données SQL Server compact de la version 7 décrite au paragraphe , page .
  • [App.config] : le fichier de configuration de l'application.
  • en [2], le dossier [lib] contient les DLL nécessaires au projet :
  • [ImpotsV7-dao] : la couche [dao] de la version 7
  • [ImpotsV7-metier] : la couche [metier] de la version 7
  • [antlr.runtime, CommonLogging, Spring.Core] pour Spring
  • en [3], les références du projet

Les couches [metier] et [dao] de la version existent déjà : ce sont celles utilisées dans les versions 7 et 8. Elles sont sous forme de DLL que nous intégrons au projet de la façon suivante :

Image non disponible
  • en [1] le dossier [lib] du serveur de la version 8 a été recopié dans le projet du service web de la version 9.
  • en [2] on modifie les propriétés de la page pour ajouter les DLL du dossier [lib] [4] aux références du projet [3].

Après cette opération, nous avons la totalité des couches nécessaires au serveur [1] ci-dessous :

Image non disponible

Si les éléments du serveur [1], [serveur], [metier], [dao], [entites], [spring] sont bien tous présents dans le projet Visual Studio, il nous manque l'élément qui va les instancier au démarrage de l'application web. Dans la version 8, une classe principale ayant la méthode statique [Main] faisait le travail d'instancier les couches avec l'aide de Spring. Dans une application web, la classe capable de faire un travail analogue est la classe associée au fichier [] :

Image non disponible
  • en [1], on ajoute un nouvel élément au projet web
  • en [2], on choisit le type Global Application Class
  • en [3], le nom proposé par défaut pour cet élément
  • en [4], on valide l'ajout
  • en [5], le nouvel élément a été intégré dans le projet

Regardons le contenu du fichier [Global.asax] :

Cacher Sélectionnez
  1. <%@ Application Language="C#" %>
  2.
  3. <script runat="server">
  4.
  5.     void Application_Start(object sender, EventArgs e)
  6.     {
  7.         // Code that runs on application startup
  8.     }
  9.
  10.     void Application_End(object sender, EventArgs e)
  11.     {
  12.         //  Code that runs on application shutdown
  13.     }
  14.
  15.     void Application_Error(object sender, EventArgs e)
  16.     {
  17.         // Code that runs when an unhandled error occurs
  18.     }
  19.
  20.     void Session_Start(object sender, EventArgs e)
  21.     {
  22.         // Code that runs when a new session is started
  23.     }
  24.
  25.     void Session_End(object sender, EventArgs e)
  26.     {
  27.         // Code that runs when a session ends.
  28.     }
  29.
  30. </script>

Le fichier est un mélange de balises à destination du serveur web (lignes 1, 3, 30) et de code C#. Cette méthode était la seule utilisée avec ASP, l'ancêtre de ASP.NET qui est la technologie actuelle de Microsoft pour la programmation web. Avec ASP.NET, cette méthode est toujours utilisable mais n'est pas la méthode par défaut. La méthode par défaut est la méthode dite du "CodeBehind" et qu'on a rencontrée dans les pages des services web, par exemple ici dans [ServiceImpot.asmx] :

Cacher Sélectionnez
  1. <%@ WebService Language="C#" CodeBehind="~/App_Code/ServiceImpot.cs" Class="ServiceImpot" %>

L'attribut CodeBehind précise où se trouve le code source de la page [ServiceImpot.asmx]. Sans cet attribut, le code source serait dans la page [ServiceImpot.asmx] avec une syntaxe analogue à celle trouvée dans [Global.asax]. Nous ne garderons pas le fichier [Global.asax] tel qu'il a été généré mais son code nous permet de comprendre à quoi il sert :

  • la classe associée à Global.asax est instanciée au démarrage de l'application. Sa durée de vie est celle de l'application tout entière. Concrètement, elle ne disparaît que lorsque le serveur web est arrêté.
  • la méthode Application_Start est exécutée ensuite. C'est la seule fois où elle le sera. Aussi l'utilise-t-on pour instancier des objets partagés entre tous les utilisateurs. Ces objets sont placés :
  • soit dans des champs statiques de la classe associée à Global.asax . Cette classe étant présente de façon permanente, toute requête de tout utilisateur peut y lire des informations.
  • soit le conteneur Application. Ce conteneur est lui aussi créé au démarrage de l'application et sa durée de vie est celle de l'application.
  • pour mettre une donnée dans ce conteneur, on écrit Application["clé"]=valeur;
  • pour la récupérer, on écrit T valeur=(T)Application["clé"]; où T est le type de valeur .
  • la méthode Session_Start est exécutée à chaque fois qu'un nouvel utilisateur fait une requête. Comment reconnaît-on un nouvel utilisateur ? Chaque utilisateur (un navigateur le plus souvent) reçoit à l'issue de sa première requête, un jeton de session qui est une chaîne de caractères unique pour chaque utilisateur. Ensuite, l'utilisateur renvoie le jeton de session qu'il a reçu à chaque nouvelle requête qu'il fait. Ceci permet au serveur web de le reconnaître. Au fil des différentes requêtes d'un même utilisateur, des données qui lui sont propres peuvent être mémorisées dans le conteneur Session :
  • pour mettre une donnée dans ce conteneur, on écrit Session["clé"]=valeur;
  • pour la récupérer, on écrit T valeur=(T)Session["clé"]; où T est le type de valeur .

La durée de vie d'une session est limitée par défaut à 20 mn d'inactivité de l'utilisateur (c.a.d. qu'il n'a pas renvoyé son jeton de session depuis 20 mn).

  • la méthode Application_Error est exécutée lorsqu'une exception non gérée par l'application web remonte jusqu'au serveur web.
  • les autres méthodes sont plus rarement utilisées.

Après ces généralités, à quoi peut nous servir Global.asax ? Nous allons utiliser sa méthode Application_Start pour initialiser les couches [metier], [dao] et [entites] contenues dans les DLL [ImpotsV7-metier, ImpotsV7-dao]. Nous utiliserons Spring pour les instancier. Les références des couches ainsi créées seront ensuite mémorisées dans des champs statiques de la classe associée à Global.asax.

Première étape, nous déportons le code C# de Global.asax dans une classe à part. Le projet évolue de la façon suivante :

Image non disponible

En [1], le fichier [Global.asax] va être associé à la classe [Global.cs] [2] en ayant l'unique ligne suivante :

Cacher Sélectionnez
  1. <%@ Application Language="C#" Inherits="WsImpot.Global"%>

L'attribut Inherits="WsImpot.Global" indique que la classe associée à Global.asax hérite de la classe WsImpot.Global . Cette classe est définie dans [Global.cs] de la façon suivante :

Cacher Sélectionnez
  1. using System;
  2. using Metier;
  3. using Spring.Context.Support;
  4. namespace WsImpot
  5. {
  6.     public class Global : System.Web.HttpApplication
  7.     {
  8.         // couche métier
  9.         public static IImpotMetier Metier;
  10.
  11.         // méthode exécutée au démarrage de l'application
  12.         private void Application_Start(object sender, EventArgs e)
  13.         {
  14.             // instanciations couches [metier] et [dao]
  15.             Metier = ContextRegistry.GetContext().GetObject("metier") as IImpotMetier;
  16.         }
  17.     }
  18. }
  • ligne 4 : l'espace de noms de la classe
  • ligne 6 : la classe Global . On peut lui donner le nom qu'on veut. L'important est qu'elle dérive de la classe System.Web.HttpApplication.
  • ligne 9 : un champ statique public qui va contenir une référence sur la couche [metier].
  • ligne 12 : la méthode Application_Start qui va être exécutée au démarrage de l'application.
  • ligne 15 : Spring est utilisé pour exploiter le fichier [web.config] dans lequel il va trouver les objets à instancier pour créer les couches [metier] et [dao]. Il n'y a aucune différence entre l'utilisation de Spring avec [App.config] dans une application windows, et celle de Spring avec [web.config] dans une application web. [web.config] et [App.config] ont par ailleurs la même structure. La ligne 15 range la référence de la couche [metier] dans le champ statique de la ligne 9, afin que cette référence soit disponible pour toutes les requêtes de tous les utilisateurs.

Le fichier [web.config] sera le suivant :

Cacher Sélectionnez
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.
  4.   <configSections>
  5.     <sectionGroup name="spring">
  6.       <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
  7.       <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
  8.     </sectionGroup>
  9.   </configSections>
  10.
  11.   <spring>
  12.     <context>
  13.       <resource uri="config://spring/objects" />
  14.     </context>
  15.     <objects xmlns="http://www.springframework.net">
  16.       <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
  17.         <constructor-arg index="0" value="MySql.Data.MySqlClient"/>
  18.         <constructor-arg index="1" value="Server=localhost;Database=bdimpots;Uid=admimpots;Pwd=mdpimpots;"/>
  19.         <constructor-arg index="2" value="select limite, coeffr, coeffn from tranches"/>
  20.       </object>
  21.       <object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
  22.         <constructor-arg index="0" ref="dao"/>
  23.       </object>
  24.     </objects>
  25.   </spring>
  26. </configuration>

C'est le fichier [App.config] utilisée dans la version 7 de l'application et étudiée au paragraphe , page .

  • lignes 16-20 : définissent une couche [dao] travaillant avec une base de données MySQL5. Cette base de données a été décrite au paragraphe , page .
  • lignes 21-23 : définissent la couche [metier]

Revenons au puzzle du serveur :

Image non disponible

Au démarrage de l'application, les couches [metier] et [dao] ont été instanciées. La durée de vie des couches est celle de l'application elle-même. Quand est instancié le service web ? En fait à chaque requête qu'on lui fait. A la fin de la requête, l'objet qui l'a servie est supprimé. Un service web est donc à première vue sans état. Il ne peut mémoriser des informations entre deux requêtes dans des champs qui lui appartiendraient. Il peut en mémoriser dans la session de l'utilisateur. Pour cela, les méthodes qu'il expose doivent être taguées avec un attribut spécial :

Cacher Sélectionnez
  1.     [WebMethod(EnableSession=true)]
  2.     public int CalculerImpot(bool marié, int nbEnfants, int salaire)
  3. ....

Ci-dessus, la ligne 1 autorise la méthode CalculerImpot à avoir accès au conteneur Session dont nous avons parlé précédemment. Nous n'aurons pas à utiliser cet attribut dans notre application. Le service web WsImpot sera donc instancié à chaque requête et sera sans état.

Nous pouvons maintenant écrire le code [ServiceImpot.cs] qui implémente le service web :

Cacher Sélectionnez
  1. using System.Web.Services;
  2. using WsImpot;
  3.
  4. [WebService(Namespace = "http://st.istia.univ-angers.fr/")]
  5. [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
  6. public class ServiceImpot : System.Web.Services.WebService
  7. {
  8.
  9.     [WebMethod]
  10.     public int CalculerImpot(bool marié, int nbEnfants, int salaire)
  11.     {
  12.         return Global.Metier.CalculerImpot(marié, nbEnfants, salaire);
  13.     }
  14.
  15. }
  • ligne 10 : l'unique méthode du service web
  • ligne 12 : on utilise la méthode CalculerImpot de la couche [metier]. Une référence de cette couche est trouvée dans le champ statique Metier de la class Global . Celle-ci appartient à l'espace de noms WsImpot (ligne 2).

Nous sommes prêts pour lancer le service web. Auparavant il faut lancer le SGBD MySQL5 afin que la base de données bdimpots soit accessible. Ceci fait, nous lançons [1] le service web :

Image non disponible

Le navigateur affiche alors la page [2]. Nous suivons le lien :

Image non disponible

Nous donnons une valeur à chacun des trois paramètres de la méthode CalculerImpot et nous demandons l'exécution de la méthode. Nous obtenons le résultat suivant qui est correct :

Image non disponible

XIII-D-2. Un client graphique windows pour le service web distant

Maintenant que le service web a été écrit, nous passons au client. Revenons sur l'architecture de l'application client / serveur :

Image non disponible

Il nous faut écrire le client [2]. L'interface graphique sera identique à celle de la version 8 :

Image non disponible

Pour écrire la partie [client] de la version 9, nous allons partir de la partie [client] de la version 8, puis nous apporterons les modifications nécessaires. Nous dupliquons le projet Visual studio étudié au paragraphe , page , le renommons ClientWsImpot et le chargeons dans Visual Studio :

Image non disponible

La solution Visual Studio de la version 8 était constituée de 2 projets :

  • le projet [metier] [1] qui était un client Tcp du serveur Tcp de calcul d'impôt
  • le projet [ui] [2] de l'interface graphique.

Les modifications à faire sont les suivantes :

  • le projet [metier] doit être désormais le client d'un service web
  • le projet [ui] doit référencer la DLL de la nouvelle couche [metier]
  • la configuration de la couche [metier] dans [App.config] doit changer.

XIII-D-2-a. La nouvelle couche [metier]

Image non disponible
  • en [1], IImpotMetier est l'interface de la couche [metier] et ImpotMetierTcp son implémentation par un client Tcp
  • en [2], nous supprimons l'implémentation ImpotMetierTcp . Nous devons créer une autre implémentation de l'interface IImpotMetier qui sera cliente d'un service web.
  • en [3], nous nommons Client l'espace de noms par défaut du projet [metier]. La DLL qui sera générée s'appellera [ImpotsV9-metier.dll].
Image non disponible
  • en [4], nous créons une référence au service web WsImpot .
  • en [5], nous le configurons et le validons.
  • en [6], la référence au service web WsImpot a été créée et un fichier [app.config] a été généré.

Dans le fichier caché [Reference.cs] :

  • l'espace de noms est Client.WsImpot
  • la classe cliente s'appelle ServiceImpotSoapClient
  • elle a une unique méthode de signature :
Cacher Sélectionnez
  1.         public int CalculerImpot(bool marié, int nbEnfants, int salaire) ;

Il nous reste à implémenter l'interface IImpotMetier :

Cacher Sélectionnez
  1. namespace Metier {
  2.     public interface IImpotMetier {
  3.         int CalculerImpot(bool marié, int nbEnfants, int salaire);
  4.     }
  5. }

Nous l'implémentons avec la classe ImpotMetierWs suivante :

Cacher Sélectionnez
  1. using System.Net.Sockets;
  2. using System.IO;
  3. using Client.WsImpot;
  4.
  5. namespace Metier {
  6.     public class ImpotMetierWs : IImpotMetier {
  7.
  8.         // client du service web distant
  9.         private ServiceImpotSoapClient client = new ServiceImpotSoapClient();
  10.
  11.         // calcul de l'impôt
  12.         public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
  13.             return client.CalculerImpot(marié, nbEnfants, salaire);
  14.         }
  15.
  16.     }
  17. }
  • ligne 6 : la classe ImpotMetierWs implémente l'interface IImpotMetier .
  • ligne 9 : à la création d'une instance ImpotMetierWs , le champ client est initialisé avec une instance d'un client du service web de calcul d'impôt.
  • ligne 12 : l'unique méthode de l'interface IImpotMetier à implémenter.
  • ligne 13 : on utilise la méthode CalculerImpot du client du service web distant de calcul d'impôt. Au final, c'est la méthode CalculerImpot du service web distant qui sera interrogée.

On peut générer la DLL du projet :

Image non disponible
  • en [1], le projet [client] dans son état final
  • en [2], génération de la DLL du projet
  • en [3], la DLL ImpotsV9-metier.dll est dans le dossier /bin/Release du projet.

XIII-D-2-b. La nouvelle couche [ui]

Image non disponible

La couche [client] du client a été écrite. Il nous reste à écrire la couche [ui]. Revenons au projet Visual studio :

Image non disponible
  • en [1], le projet [ui] issu de la version 8
  • en [2], la DLL ImpotsV8-metier de l'ancienne couche [metier] est remplacée par la DLL ImpotsV9-metier de la nouvelle couche
  • en [3], la DLL ImpotsV9-metier est ajoutée aux références du projet.

Le second changement intervient dans [App.config]. Il faut se rappeler que ce fichier est utilisé par Spring pour instancier la couche [metier]. Comme celle-ci a changé, la configuration de [App.config] doit changer. D'autre part, [App.config] doit avoir la configuration permettant de joindre le service web distant de calcul d'impôt. Cette configuration a été générée dans le fichier [app.config] du projet [metier] lorsque la référence au service web distant y a été ajoutée.

Le fichier [App.config] devient donc le suivant :

Cacher Sélectionnez
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.
  4.     <configSections>
  5.         <sectionGroup name="spring">
  6.             <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
  7.             <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
  8.         </sectionGroup>
  9.     </configSections>
  10.
  11.     <spring>
  12.         <context>
  13.             <resource uri="config://spring/objects" />
  14.         </context>
  15.         <objects xmlns="http://www.springframework.net">
  16.             <object name="metier" type="Metier.ImpotMetierWs, ImpotsV9-metier">
  17.             </object>
  18.         </objects>
  19.     </spring>
  20.
  21.     <!-- service web -->
  22.     <system.serviceModel>
  23.         <bindings>
  24.             <basicHttpBinding>
  25.                 <binding name="ServiceImpotSoap" closeTimeout="00:01:00" openTimeout="00:01:00"
  26.                         receiveTimeout="00:10:00" sendTimeout="00:01:00" allowCookies="false"
  27.                         bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard"
  28.                         maxBufferSize="65536" maxBufferPoolSize="524288" maxReceivedMessageSize="65536"
  29.                         messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered"
  30.                         useDefaultWebProxy="true">
  31.                     <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
  32.                             maxBytesPerRead="4096" maxNameTableCharCount="16384" />
  33.                     <security mode="None">
  34.                         <transport clientCredentialType="None" proxyCredentialType="None"
  35.                                 realm="" />
  36.                         <message clientCredentialType="UserName" algorithmSuite="Default" />
  37.                     </security>
  38.                 </binding>
  39.             </basicHttpBinding>
  40.         </bindings>
  41.         <client>
  42.             <endpoint address="http://localhost:2172/WsImpot/ServiceImpot.asmx"
  43.                     binding="basicHttpBinding" bindingConfiguration="ServiceImpotSoap"
  44.                     contract="WsImpot.ServiceImpotSoap" name="ServiceImpotSoap" />
  45.         </client>
  46.     </system.serviceModel>
  47. </configuration>
  • lignes 15-18 : Spring n'instancie qu'un objet, la couche [metier]
  • ligne 16 : la couche [metier] est instanciée par la classe [Metier.ImpotMetierWs] qui se trouve dans la DLL ImpotsV9-metier .
  • lignes 22-46 : la configuration du client du service web distant. C'est un copier / coller du contenu du fichier [app.config] du projet [metier].

On est prêts. On exécute l'application par Ctrl-F5 ( le service web doit être lancé, le SGBD MySQL5 doit être lancé, le port ligne 42 ci-dessus doit être le bon) :

Image non disponible

XIII-E. Un client web pour le service web de calcul d'impôt

Reprenons l'architecture de l'application client / serveur qui vient d'être écrite :

Image non disponible

La couche [ui] ci-dessus était implémentée par un client graphique windows. Nous l'implémentons maintenant avec une interface web :

Image non disponible

C'est un changement important pour les utilisateurs. Actuellement notre application client / serveur, version 9, peut servir plusieurs clients simultanément. C'est une amélioration vis à vis de la version 8 où elle ne servait qu'un client à la fois. La contrainte est que les utilisateurs désireux d'utiliser le service web de calcul d'impôt doivent disposer sur leur poste du client windows que nous avons écrit. Dans cette nouvelle version, que nous appellerons version 10, les utilisateurs pourront avoir accès, avec leur navigateur, au service web de calcul d'impôt.

Dans l'architecture ci-dessus :

  • le côté serveur ne bouge pas. Il reste ce qu'il est dans la version 9.
  • côté client, la couche [client du service web] ne bouge pas. Elle a été encapsulée dans la DLL [ImpotsV9-metier]. Nous allons réutiliser cette DLL.
  • finalement le seul changement consiste à remplacer une interface graphique windows par une interface web.

Nous allons aborder de nouvelles notions de programmation web côté serveur. Le but de ce document n'étant pas d'enseigner la programmation web, nous essaierons d'expliquer la démarche qui va être suivie mais sans entrer dans les détails. Il y aura donc un côté un peu "magique" dans cette section. Il nous semble cependant intéressant de faire cette démarche pour montrer un nouvel exemple d'architecture multicouches où l'une des couches est changée.

L'architecture de la version 10 est donc la suivante :

Image non disponible

Nous avons déjà toutes les couches, sauf celle de la couche [web]. Pour mieux comprendre ce qui va être fait, nous avons besoin d'être plus précis sur l'architecture du client. Elle sera la suivante :

Image non disponible
  • l'utilisateur web a dans son navigateur un formulaire web
  • ce formulaire est posté au serveur web 1 qui le fait traiter par la couche [web]
  • la couche [web] aura besoin des services du client du service web distant, encapsulé dans [ImpotsV9-metier.dll].
  • le client du service web distant communiquera avec le serveur web 2 qui héberge le service web distant.
  • ,a réponse du service web distant va remonter jusqu'à la couche web du client qui va la mettre en forme dans une page qu'il va envoyer à l'utilisateur.

Notre travail ici est donc :

  • de construire le formulaire web que verra l'utilisateur dans son navigateur
  • d'écrire l'application web qui va traiter la demande de l'utilisateur et lui envoyer une réponse sous la forme d'une nouvelle page web. Celle-ci sera en fait la même que le formulaire dans lequel on aura rajouté le montant de l'impôt à payer
  • d'écrire la "glue" qui fait que tout ça marche ensemble.

Tout ceci sera fait à l'aide d'un nouveau site web créé avec Visual Web Developer :

Image non disponible
  • [1] : prendre l'option File / New Web Site
  • [2] : choisir une application de type ASP.NET Web Site
  • [3] : choisir le langage de développement : C#
  • [4] : indiquer le dossier où créer le projet
Image non disponible
  • [5] : le projet créé dans Visual Web Developer
  • [Default.aspx] est une page web appelée la page par défaut. C'est celle qui sera délivrée si on demande l'Url http://…/ClientAspImpot sans préciser de document. C'est cette page qui contiendra le formulaire de calcul de l'impôt que l'utilisateur verra dans son navigateur.
  • [Default.aspx.cs] est la classe associée à la page, celle qui va générer le formulaire envoyé à l'utilisateur puis le traiter lorsque celui-ci l'aura rempli et validé.
  • [web.config] est le fichier de configuration de l'application. Contrairement aux fois précédentes nous allons le garder.

Si nous revenons à l'architecture que nous devons construire :

Image non disponible
  • [1] va être implémentée par [Default.aspx]
  • [2] va être implémentée par [Default.aspx.cs]
  • [3] va être implémentée par la DLL [ImpotV9-metier]

Commençons par implémenter la couche [3]. Il y a plusieurs étapes :

Image non disponible
  • en [1], le dossier [lib] du client graphique windows version 9 est copié dans le dossier du projet web [ClientAspWsImpot]. Cela se fait avec l'explorateur windows. Pour faire apparaître ce dossier dans la solution Web Developer, il faut rafraîchir la solution avec le bouton [2].
  • puis les ajouter aux références du projet [3,4,5]. Les Dll référencées sont automatiquement recopiées dans le dossier /bin du projet [6].

On a désormais les Dll nécessaires au fonctionnement de Spring et la couche client du service web distant est également implémentée. Si le code de celui-ci est bien présent, sa configuration reste à faire. Dans la version 9, il était configuré par le fichier [App.config] suivant :

Cacher Sélectionnez
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.
  4.     <configSections>
  5.         <sectionGroup name="spring">
  6.             <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
  7.             <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
  8.         </sectionGroup>
  9.     </configSections>
  10.
  11.     <spring>
  12.         <context>
  13.             <resource uri="config://spring/objects" />
  14.         </context>
  15.         <objects xmlns="http://www.springframework.net">
  16.             <object name="metier" type="Metier.ImpotMetierWs, ImpotsV9-metier">
  17.             </object>
  18.         </objects>
  19.     </spring>
  20.
  21.     <!-- service web -->
  22.     <system.serviceModel>
  23.         <bindings>
  24.             <basicHttpBinding>
  25. ...
  26.             </basicHttpBinding>
  27.         </bindings>
  28.         <client>
  29.             <endpoint address="http://localhost:2172/WsImpot/ServiceImpot.asmx"
  30.                     binding="basicHttpBinding" bindingConfiguration="ServiceImpotSoap"
  31.                     contract="WsImpot.ServiceImpotSoap" name="ServiceImpotSoap" />
  32.         </client>
  33.     </system.serviceModel>
  34. </configuration>

Nous reprenons cette configuration à l'identique et l'intégrons au fichier [web.config] de la façon suivante :

Cacher Sélectionnez
  1. <?xml version="1.0"?>
  2. <configuration>
  3.
  4.
  5.     <configSections>
  6.       <sectionGroup name="system.web.extensions"...>
  7. ...
  8.       </sectionGroup>
  9.       <!-- début section Spring -->
  10.         <sectionGroup name="spring">
  11.           <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
  12.           <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
  13.         </sectionGroup>
  14.       <!-- fin section Spring -->
  15.     </configSections>
  16.
  17.   <!-- début configuration Spring -->
  18.   <spring>
  19.     <context>
  20.       <resource uri="config://spring/objects" />
  21.     </context>
  22.     <objects xmlns="http://www.springframework.net">
  23.       <object name="metier" type="Metier.ImpotMetierWs, ImpotsV9-metier">
  24.       </object>
  25.     </objects>
  26.   </spring>
  27.   <!-- fin configuration Spring -->
  28.
  29.   <!-- début configuration client du service web distant -->
  30.   <system.serviceModel>
  31.     <bindings>
  32.       <basicHttpBinding>
  33. ...
  34.       </basicHttpBinding>
  35.     </bindings>
  36.     <client>
  37.       <endpoint address="http://localhost:2172/WsImpot/ServiceImpot.asmx"
  38.                     binding="basicHttpBinding" bindingConfiguration="ServiceImpotSoap"
  39.                     contract="WsImpot.ServiceImpotSoap" name="ServiceImpotSoap" />
  40.     </client>
  41.   </system.serviceModel>
  42.   <!-- fin configuration client du service web distant -->
  43.
  44.   <!-- autres configurations déjà présentes dans le web.config généré -->
  45. ...
  46. </configuration>

On notera que la ligne 37 référence le port du service web distant. Ce port peut changer puisque Visual Developer lance le service web sur un port aléatoire.

Revenons à l'architecture du client web que nous devons construire :

Image non disponible
  • [1] va être implémentée par [Default.aspx]
  • [2] va être implémentée par [Default.aspx.cs]
  • [3] a été implémentée par la DLL [ImpotV9-metier]

Nous venons d'implémenter la couche [3]. Nous passons à l'interface web [1] implémentée par la page [Default.aspx]. Double-cliquons sur la page [Default.aspx] pour passer en mode conception.

Image non disponible

Il y a deux façons de construire une page web :

  • graphiquement comme en [2]. Il faut alors choisir le mode [Design] en [1]. On trouvera cette barre de boutons en bas dans la barre d'état de l'éditeur de la page web.
  • avec un langage de balises comme en [3]. Il faut alors choisir le mode [Source] en [1].

Les modes [Design] et [Source] sont bidirectionnels : une modification faite en mode [Design] se traduit par une modification en mode [Source] et vice-versa. Rappelons que le formulaire web à présenter dans le navigateur est le suivant :

Image non disponible
  • en [1], le formulaire affiché dans un navigateur
  • en [2], les composants utilisés pour le construire
  • en [3], la page de conception du formulaire. Il comprend les éléments suivants :
  • ligne A, deux boutons radio nommés RadioButtonOui et RadioButtonNon
  • ligne B, une zone de saisie nommée TextBoxEnfants et un label nommé LabelErreurEnfants
  • ligne C, une zone de saisie nommée TextBoxSalaire et un label nommé LabelErreurSalaire
  • ligne D, un label nommé LabelImpot
  • ligne E, deux boutons nommés ButtonCalculer et ButtonEffacer

Une fois un composant déposé sur la surface de conception, on a accès à ses propriétés :

Image non disponible
  • en [1], accès aux propriétés d'un composant
  • en [2], la fiche des propriétés du composant [LabelErreurEnfants ]
  • en [3], (ID) est le nom du composant
  • en [4], nous avons donné la couleur rouge aux caractères du label.

Il n'est pas suffisant de déposer des composants sur le formulaire puis de fixer leurs propriétés. Il faut également organiser leur disposition. Dans une interface graphique windows, cette disposition est absolue. On fait glisser le composant là où on veut qu'il soit. Dans une page web, c'est différent, plus complexe mais aussi plus puissant. Cet aspect ne sera pas abordé ici.

Le code source [Default.aspx] généré par cette conception est le suivant :

Cacher Sélectionnez
  1. <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>
  2.
  3. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  4. <html xmlns="http://www.w3.org/1999/xhtml">
  5. <head runat="server">
  6.   <title>Calculer votre impôt</title>
  7. </head>
  8. <body bgcolor="#ffff99">
  9.   <h2>
  10.     Calculer votre impôt</h2>
  11.   <form id="form1" runat="server">
  12.   <asp:ScriptManager ID="ScriptManager2" runat="server" EnablePartialRendering="true" />
  13.   <asp:UpdatePanel runat="server" ID="UpdatePanelPam">
  14.     <ContentTemplate>
  15.       <div>
  16.       </div>
  17.       <table>
  18.         <tr>
  19.           <td>
  20.             Etes-vous marié(e)
  21.           </td>
  22.           <td>
  23.             <asp:RadioButton ID="RadioButtonOui" runat="server" GroupName="statut" Text="Oui" />
  24.             <asp:RadioButton ID="RadioButtonNon" runat="server" GroupName="statut" Text="Non"
  25.               Checked="True" />
  26.           </td>
  27.         </tr>
  28.         <tr>
  29.           <td>
  30.             Nombre d&#39;enfants
  31.           </td>
  32.           <td>
  33.             <asp:TextBox ID="TextBoxEnfants" runat="server" Columns="3"></asp:TextBox>
  34.           </td>
  35.           <td>
  36.             <asp:Label ID="LabelErreurEnfants" runat="server" ForeColor="#FF3300"></asp:Label>
  37.           </td>
  38.         </tr>
  39.         <tr>
  40.           <td>
  41.             Salaire annuel
  42.           </td>
  43.           <td>
  44.             <asp:TextBox ID="TextBoxSalaire" runat="server" Columns="8"></asp:TextBox>
  45.           </td>
  46.           <td>
  47.             <asp:Label ID="LabelErreurSalaire" runat="server" ForeColor="#FF3300"></asp:Label>
  48.           </td>
  49.         </tr>
  50.         <tr>
  51.           <td>
  52.             Impôt à payer
  53.           </td>
  54.           <td>
  55.             <asp:Label ID="LabelImpot" runat="server" BackColor="#99CCFF"></asp:Label>
  56.           </td>
  57.         </tr>
  58.       </table>
  59.       <br />
  60.       <table>
  61.         <tr>
  62.           <td>
  63.             <asp:Button ID="ButtonCalculer" runat="server" Text="Calculer" OnClick="ButtonCalculer_Click" />
  64.           </td>
  65.           <td>
  66.             <asp:Button ID="ButtonEffacer" runat="server" Text="Effacer" OnClick="ButtonEffacer_Click" />
  67.           </td>
  68.           <td>
  69.             &nbsp;
  70.           </td>
  71.         </tr>
  72.       </table>
  73.       </div>
  74.     </ContentTemplate>
  75.   </asp:UpdatePanel>
  76.   </form>
  77. </body>
  78. </html>

On reconnaît les composants du formulaire aux lignes 23, 24, 33, 36, 44, 47, 55, 63 et 66. Le reste est essentiellement de la mise en forme.

Revenons à l'architecture que nous devons construire :

Image non disponible
  • [1] a été implémentée par [Default.aspx]
  • [2] va être implémentée par [Default.aspx.cs]
  • [3] a été implémentée par la DLL [ImpotV9-metier]

Les couches [1] et [3] sont désormais implémentées. Il nous reste à écrire la couche [2], celle qui génère le formulaire, l'envoie à l'utilisateur, le traite lorsque celui-ci le lui renvoie rempli, utilise la couche [3] pour le calcul de l'impôt, génère la page web de réponse à l'utilisateur et la lui renvoie. C'est le code [Default.aspx.cs] qui fait tout ce travail :

Cacher Sélectionnez
  1. using System;
  2. using WsImpot;
  3.
  4. public partial class _Default : System.Web.UI.Page
  5. {
  6.     protected void ButtonCalculer_Click(object sender, EventArgs e)
  7.     {
  8.   ...
  9.     }
  10.     protected void ButtonEffacer_Click(object sender, EventArgs e)
  11.     {
  12. ...
  13.     }
  14. }

C'est un code très proche de celui d'un formulaire windows classique. C'est l'avantage principal de la technologie ASP.NET : il n'y a pas de rupture entre le modèle de programmation windows et celui de la programmation web ASP.NET. Il faut simplement toujours se souvenir du schéma suivant :

Image non disponible

Lorsque qu'en [1], l'utilisateur va cliquer sur le bouton [Calculer], la procédure ButtonCalculer_Click de la ligne 6 de [Default.aspx.cs] va être exécutée. Mais entre-temps :

  • les valeurs du formulaire rempli vont transiter du navigateur au serveur web via le protocole Http
  • le serveur ASP.NET va analyser la demande et la transférer à la page [Default.aspx]
  • la page [Default.aspx] va être instanciée.
  • ses composants ( RadioButtonOui , RadioButtonNon , TextBoxEnfants , TextBoxSalaire , LabelErreurEnfants , LabelErreurSalaire , LabelImpot ) vont être initialisés avec la valeur qu'ils avaient lorsque le formulaire a été envoyé initialement au navigateur grâce à un mécanisme appelé "ViewState".
  • les valeurs postées vont être affectées à leurs composants ( RadioButtonOui , RadioButtonNon , TextBoxEnfants , TextBoxSalaire ). Ainsi si l'utilisateur a mis 2 comme nombre d'enfants, on aura TextBoxEnfants.Text="2" .
  • si la page [Default.aspx] a une méthode [Page_Load], celle-ci sera exécutée
  • la méthode [ButtonCalculer_Click] de la ligne 6 sera exécutée si c'est le bouton [Calculer] qui a été cliqué
  • la méthode [ButtonEffacer_Click] de la ligne 10 sera exécutée si c'est le bouton [Effacer] qui a été cliqué

Entre le moment où l'utilisateur crée un événement dans son navigateur et celui où il est traité dans [Default.aspx.cs], il y a une grande complexité. Celle-ci est cachée et on peut faire comme si elle n'existait pas lorsqu'on écrit les gestionnaires d'événements de la page web. Mais on ne doit jamais oublier qu'il y a le réseau entre l'événement et son gestionnaire et qu'il n'est donc pas question de gérer des événements souris tels Mouse_Move qui provoqueraient des aller /retour client / serveur coûteux …

Le code des gestionnaires des clics sur les boutons [Calculer] et [Effacer] est celui qu'on aurait écrit pour une application windows classique :

Cacher Sélectionnez
  1. protected void ButtonCalculer_Click(object sender, EventArgs e)
  2.     {
  3.         // vérification données
  4.         int nbEnfants;
  5.         bool erreur = false;
  6.         if (!int.TryParse(TextBoxEnfants.Text.Trim(), out nbEnfants) || nbEnfants < 0)
  7.         {
  8.             LabelErreurEnfants.Text = "Valeur incorrecte...";
  9.             erreur = true;
  10.         }
  11.         int salaire;
  12.         if (!int.TryParse(TextBoxSalaire.Text.Trim(), out salaire) || salaire < 0)
  13.         {
  14.             LabelErreurSalaire.Text = "Valeur incorrecte...";
  15.             erreur = true;
  16.         }
  17.         // erreur ?
  18.         if (erreur) return;
  19.         // on efface les éventuelles erreurs
  20.         LabelErreurEnfants.Text = "";
  21.         LabelErreurSalaire.Text = "";
  22.         // état marital
  23.         bool marié = RadioButtonOui.Checked;
  24.         // calcul de l'impôt
  25.         try
  26.         {
  27.             LabelImpot.Text = String.Format("{0} euros",Global.Metier.CalculerImpot(marié, nbEnfants, salaire));
  28.         }
  29.         catch (Exception ex)
  30.         {
  31.             LabelImpot.Text = ex.Message;
  32.         }
  33.     }
  • pour comprendre ce code, il faut savoir
  • qu'au début de son exécution, le formulaire [Default.aspx] est tel que l'utilisateur l'a rempli. Ainsi les champs ( RadioButtonOui , RadioButtonNon , TextBoxEnfants , TextBoxSalaire ) ont les valeurs saisies par l'utilisateur.
  • qu'à l'issue de son exécution, la même page [Default.aspx] va être renvoyée à l'utilisateur. Cela est fait de façon automatique.

La procédure ButtonCalculer_Click doit donc à partir des valeurs actuelles des champs ( RadioButtonOui , RadioButtonNon , TextBoxEnfants , TextBoxSalaire ) fixer la valeur de tous les champs ( RadioButtonOui , RadioButtonNon , TextBoxEnfants , TextBoxSalaire , LabelErreurEnfants , LabelErreurSalaire , LabelImpot ) de la nouvelle page [Default.aspx] qui va être renvoyée à l'utilisateur.

Il n'y a pas de difficulté particulière à ce code. Seule la ligne 27 mérite d'être expliquée. Elle utilise la méthode CalculerImpot d'un champ Global.Metier qui n'a pas été rencontré. Nous allons y revenir prochainement.

La méthode ButtonEffacer_Click est la suivante :

Cacher Sélectionnez
  1.     protected void ButtonEffacer_Click(object sender, EventArgs e)
  2.     {
  3.         // raz formulaire
  4.         TextBoxEnfants.Text = "";
  5.         TextBoxSalaire.Text = "";
  6.         LabelImpot.Text = "";
  7.         LabelErreurEnfants.Text = "";
  8.         LabelErreurSalaire.Text = "";
  9. }

Revenons à l'architecture que nous devons construire :

Image non disponible
  • [1] a été implémentée par [Default.aspx]
  • [2] a été implémentée par [Default.aspx.cs]
  • [3] a été implémentée par la DLL [ImpotV9-metier]

Il nous reste à mettre la "glue" autour de ces trois couches. Il s'agit essentiellement :

  • d'instancier la couche [3] au démarrage de l'application
  • d'en mettre une référence dans un endroit où la page web [Default.aspx.cs] pourra aller la chercher à chaque fois qu'elle sera instanciée et qu'on lui demandera de calculer l'impôt.

Ce n'est pas un problème nouveau. Il a déjà été rencontré dans la construction du service web distant et étudié au paragraphe , page . On sait que la solution consiste :

  • à créer un fichier [Global.asax] associé à une classe [Global.cs]
  • à instancier la couche [3] dans la méthode Application_Start de [Global.cs]
  • à mettre la référence de la couche [3] dans un champ statique de la classe [Global.cs], car la durée de vie de cette classe est celle de l'application.

Aussi notre projet web évolue-t-il de la façon suivante :

Image non disponible
  • en [1], le fichier [Global.asax].
  • en [2], le code [Global.cs] associé. Le dossier [App_Code] dans lequel se trouve ce fichier n'est pas présent par défaut dans la solution web. Utiliser [3] pour le créer.

Le fichier Global.asax est le suivant :

Cacher Sélectionnez
  1. <%@ Application Language="C#" Inherits="WsImpot.Global"%>

Le code [Global.cs] est le suivant :

Cacher Sélectionnez
  1. using System;
  2. using Metier;
  3. using Spring.Context.Support;
  4. namespace WsImpot
  5. {
  6.     public class Global : System.Web.HttpApplication
  7.     {
  8.         // couche métier
  9.         public static IImpotMetier Metier;
  10.
  11.         // méthode exécutée au démarrage de l'application
  12.         private void Application_Start(object sender, EventArgs e)
  13.         {
  14.             // instanciations couches [metier] et [dao]
  15.             Metier = ContextRegistry.GetContext().GetObject("metier") as IImpotMetier;
  16.         }
  17.     }
  18. }
  • ligne 6 : la classe s'appelle Global et fait partie de l'espace de noms WsImpot (ligne 4). Aussi son nom complet est-il WsImpot.Global et c'est ce nom qu'il faut mettre dans l'attribut Inherits de Global.asax .
  • ligne 6 : on sait que la classe associée à Global.asax doit obligatoirement dériver de la classe System.Web.HttpApplication .
  • ligne 12 : la méthode Application_Start exécutée au démarrage de l'application web.
  • ligne 15 : on intancie la couche [metier] (couche [3] de l'application en cours de construction) à l'aide de Spring et de la configuration suivante dans [web.config] :
Cacher Sélectionnez
  1.   <!-- objets Spring -->
  2.   <spring>
  3.     <context>
  4.       <resource uri="config://spring/objects" />
  5.     </context>
  6.     <objects xmlns="http://www.springframework.net">
  7.       <object name="metier" type="Metier.ImpotMetierWS, ImpotsV9-metier">
  8.       </object>
  9.     </objects>
  10. </spring>

La classe [Metier.ImpotMetierWS] de la ligne (g) ci-dessus se trouve dans [ImpotsV9-metier.dll].

La référence de la couche [metier] créée est mise dans le champ statique de la ligne 9. C'est ce champ qui est utilisé dans la ligne 27 de la procédure ButtonCalculer_Click :

Cacher Sélectionnez
  1. LabelImpot.Text = String.Format("{0} euros",Global.Metier.CalculerImpot(marié, nbEnfants, salaire));

Nous sommes prêts pour un test. Il faut lancer le SGBD MySQL5, le service web distant et noter le port sur lequel il opère :

Image non disponible

Ceci fait, il faut vérifier que dans le fichier [web.config] du client web, le port du service web distant est bon :

Cacher Sélectionnez
  1. ...
  2.     <client>
  3.       <endpoint address="http://localhost:2172/WsImpot/ServiceImpot.asmx"
  4.                     binding="basicHttpBinding" bindingConfiguration="ServiceImpotSoap"
  5.                     contract="WsImpot.ServiceImpotSoap" name="ServiceImpotSoap" />
  6.     </client>
  7.   </system.serviceModel>

Ceci fait, le client web du service web distant peut être lancé par Ctrl-F5 :

Image non disponible

XIII-F. Un client console Java pour le service web de calcul d'impôt

Afin de montrer que les services web sont accessibles par des clients écrits dans un langage quelconque, nous écrivons un client Java console basique. L'architecture de l'application client / serveur sera la suivante :

Image non disponible
  • le client [1] sera écrit en Java
  • le serveur [2] est celui écrit en C#

Tout d'abord, nous allons changer un détail dans notre service web de calcul d'impôt. Sa définition actuelle dans [ServiceImpot.cs] est la suivante :

Cacher Sélectionnez
  1. ...
  2. public class ServiceImpot : System.Web.Services.WebService
  3. {
  4.
  5.     [WebMethod]
  6.     public int CalculerImpot(bool marié, int nbEnfants, int salaire)
  7.     {
  8.         return Global.Metier.CalculerImpot(marié, nbEnfants, salaire);
  9.     }
  10.
  11. }

Les tests ont montré que l'accent du paramètre marié des lignes 6 et 8 pouvait être un problème dans l'interopérabilité Java / C#. Nous adopterons la nouvelle définition suivante :

Cacher Sélectionnez
  1. ...
  2. public class ServiceImpot : System.Web.Services.WebService
  3. {
  4.
  5.     [WebMethod]
  6.     public int CalculerImpot(bool marie, int nbEnfants, int salaire)
  7.     {
  8.         return Global.Metier.CalculerImpot(marie, nbEnfants, salaire);
  9.     }
  10.
  11. }

Ce service sera placé dans un nouveau projet Web Developer appelé WsImpotsSansAccents . Le service web aura alors l'Url [/WsImpotSansAccents/ServiceImpot.asmx].

Image non disponible

Pour écrire le client Java, nous utiliserons l'IDE Netbeans [http://www.netbeans.org/] :

Image non disponible
  • en [1], créer un nouveau projet
  • en [2,3], choisir un projet Java de type Java Application .
  • en [4], passer à l'étape suivante
  • en [5], donner un nom au projet
  • en [6], indiquer le dossier où un sous-dossier portant le nom du projet sera créé pour lui
  • en [7], donner un nom à la classe qui va contenir la méthode main exécutée au démarrage de l'application
  • en [8], terminer l'assistant
Image non disponible
  • en [9] : le projet Java généré
  • en [10] : clic droit sur le projet pour générer le client du service web de calcul d'impôt
Image non disponible
  • en [11], l'Url du fichier décrivant le service web de calcul d'impôt :

http://localhost:1089/WsImpotSansAccents/ServiceImpot.asmx?WSDL

Cette Url est celle du service [ServiceImpot.asmx] à laquelle on ajoute le paramètre ?WSDL. Le document situé à cette Url décrit en langage Xml ce que sait faire le service [15]. C'est un élément standard d'un service web.

  • en [12], le package (équivalent de l'espace de noms du C#) dans lequel mettre les classes qui vont être générées
  • en [13], laisser la valeur par défaut
  • en [14], terminer l'assistant
Image non disponible
  • en [16], le service web importé a été intégré au projet Java. Il supporte deux protocoles de communication Soap et Soap12 .
  • en [17], la classe [Main] dans laquelle nous allons utiliser le client généré
Image non disponible
  • en [18], nous allons insérer du code dans la méthode [main]. Mettre le curseur à l'endroit où le code doit être inséré, cliquer droit et prendre l'option [19]
  • en [20], indiquer que vous voulez générer le code d'appel de la fonction CalculerImpot du service distant de calcul d'impôt puis faire Ok.

Le code généré dans [Main] est le suivant :

Cacher Sélectionnez
  1. public class Main {
  2.
  3.     public static void main(String[] args) {
  4.         // TODO code application logic here
  5.       try { // Call Web Service Operation
  6.         wsimpot.ServiceImpot service = new wsimpot.ServiceImpot();
  7.         wsimpot.ServiceImpotSoap port = service.getServiceImpotSoap();
  8.         // TODO initialize WS operation arguments here
  9.         boolean marie = false;
  10.         int nbEnfants = 0;
  11.         int salaire = 0;
  12.         // TODO process result here
  13.         int result = port.calculerImpot(marie, nbEnfants, salaire);
  14.         System.out.println("Result = "+result);
  15.       } catch (Exception ex) {
  16.         // TODO handle custom exceptions here
  17.       }
  18.     }
  19. }

Le code généré montre comment appeler à la fonction CalculerImpot du service distant de calcul d'impôt. Si on fait le parallèle avec ce qui a été vu en C#, la variable port de la ligne 7 est l'équivalent du client utilisé en C#. Nous ne commenterons pas davantage ce code. Nous le réaménageons de la façon suivante :

Cacher Sélectionnez
  1. import wsimpot.ServiceImpot;
  2. public class Main {
  3.     public static void main(String[] args) {
  4.       try {
  5.         // on appelle la fonction CalculerImpot du service web
  6.         System.out.println(String.format("Montant à payer : %d euros", new ServiceImpot().getServiceImpotSoap().calculerImpot(true, 2, 60000)));
  7.       } catch (Exception ex) {
  8.         System.out.println(String.format("L'erreur suivante s'est produite %s",ex.getMessage()));
  9.       }
  10.     }
  11. }
  • ligne 1 : nous importons la classe ServiceImpot qui représente le client généré par l'assistant.
  • ligne 6 : nous appelons la méthode distante CalculerImpot en suivant la procédure indiquée dans le code généré dans main.

Les résultats obtenus dans la console à l'exécution (F6) sont les suivants :

Cacher Sélectionnez
  1. init:
  2. deps-jar:
  3. wsimport-init:
  4. wsimport-client-check-ServiceImpot.asmx:
  5. wsimport-client-ServiceImpot.asmx:
  6. wsimport-client-generate:
  7. wsimport-client-compile:
  8. Compiling 1 source file to C:\data\2007-2008\netbeans\ClientNetbeansPourServiceImpotDotNet\build\classes
  9. compile:
  10. run:
  11. Montant à payer : 4282 euros
  12. BUILD SUCCESSFUL (total time: 7 seconds)


CSharp tutoriel 2008

Apprentissage du langage C#

  • XIV. A suivre…

XIV. A suivre…

Il resterait des thèmes importants à couvrir. En voici trois :

  • LINQ (Language INtegrated Query) qui permet de requêter des collections d'objets, des flux XML et la base de données SQL Server avec un langage unifié similaire à SQL.
  • une étude de XML avec les classes .NET permettant de gérer les documents XML.
  • la programmation Web avec les pages et contrôles ASP.NET.
  • .NET Remoting qui permet de faire du client / serveur avec un protocole propriétaire mais efficace
  • …

Le point 3 est développé dans les documents suivants écrits en 2004 mais sans doute encore d'actualité en 2008 :

  • Programmation ASP.NET / VB.NET, volume 1, [http://tahe.developpez.com/dotnet/aspnet/vol1/]
  • Programmation ASP.NET / VB.NET , volume 2, [http://tahe.developpez.com/dotnet/aspnet/vol2/]

Les exemples de ces documents sont en VB.NET.

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2013 Serge Tahé. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Francis Walter -


CSharp tutoriel 2008

Apprentissage du langage C#

  • XV. Annexes
    • XV-A. Le SGBD SQL Server Express 2005
      • XV-A-1. Installation
      • XV-A-2. Lancer / Arrêter SQL Server
      • XV-A-3. Création d'un utilisateur jpa et d'une base de données jpa
      • XV-A-4. Création de la table [ARTICLES] de la base de données jpa
      • XV-A-5. Le connecteur ADO.NET de SQL Server Express
    • XV-B. Le SGBD MySQL5
      • XV-B-1. Installation
      • XV-B-2. Lancer / Arrêter MySQL5
      • XV-B-3. Clients d'administration MySQL
      • XV-B-4. Création d'un utilisateur jpa et d'une base de données jpa
      • XV-B-5. Installation du connecteur ADO.NET de MySQL5
      • XV-B-6. Installation du pilote ODBC de MySQL5

XV. Annexes

XV-A. Le SGBD SQL Server Express 2005

XV-A-1. Installation

Le SGBD SQL Server Express 2005 est disponible à l'url [http://msdn.microsoft.com/vstudio/express/sql/download/] :

Image non disponible
  • en [1] : d'abord télécharger et installer la plate-forme .NET 2.0
  • en [2] : puis installer et télécharger SQL Server Express 2005
  • en [3] : puis installer et télécharger SQL Server Management Studio Express qui permet d'administrer SQL Server

L'installation de SQL Server Express donne naissance à un dossier dans [Démarrer / Programmes ] :

Image non disponible
  • en [1] : l'application de configuration de SQL Server. Permet également de lancer / arrêter le serveur
  • en [2] : l'application d'administration du serveur

XV-A-2. Lancer / Arrêter SQL Server

Comme pour les SGBD précédents, SQL server Express a été installé comme un service windows à démarrage automatique. Nous changeons cette configuration :

[Démarrer / Panneau de configuration / Performances et maintenance / Outils d'administration / Services ] :

Image non disponible
  • en [1] : nous double-cliquons sur [Services]
  • en [2] : on voit qu'un service appelé [SQL Server] est présent, qu'il est démarré [3] et que son démarrage est automatique [4].
  • en [5] : un autre service lié à SQL Server, appelé "SQL Server Browser" est également actif et à démarrage automatique.

Pour modifier ce fonctionnement, nous double-cliquons sur le service [SQL Server] :

Image non disponible
  • en [1] : on met le service en démarrage manuel
  • en [2] : on l'arrête
  • en [3] : on valide la nouvelle configuration du service

On procèdera de même avec le service [SQL Server Browser] (cf [5] plus haut). Pour lancer et arrêter manuellement le service OracleServiceXE, on pourra utiliser l'application [1] du dossier [SQL server] :

Image non disponible
Image non disponible
  • en [1] : s'assurer que le protocole TCP/IP est actif (enabled) puis passer aux propriétés du protocole.
  • en [2] : dans l'onglet [IP Addresses], option [IPAll] :
  • le champ [TCP Dynamic ports] est laissé vide
  • le port d'écoute du serveur est fixé à 1433 dans [TCP Port]
Image non disponible
  • en [3] : un clic droit sur le service [SQL Server] donne accès aux options de démarrage / arrêt du serveur. Ici, on le lance.
  • en [4] : SQL Server est lancé

XV-A-3. Création d'un utilisateur jpa et d'une base de données jpa

Lançons le SGBD comme indiqué ci-dessus, puis l'application d'administration [1] via le menu ci-dessous :

Image non disponible
Image non disponible
  • en [1] : on se connecte à SQL Server en tant qu'administrateur Windows
  • en [2] : on configure les propriétés de la connexion
Image non disponible
  • en [3] : on autorise un mode mixte de connexion au serveur : soit avec un login windows (un utilisateur windows), soit avec un login SQL Server (compte défini au sein de SQL Server, indépendant de tout compte windows).
  • en [3b] : on crée un utilisateur SQL Server
Image non disponible
  • en [4] : option [General]
  • en [5] : le login
  • en [6] : le mot de passe (jpa ici)
  • en [7] : option [Server Roles]
  • en [8] : l'utilisateur jpa aura le droit de créer des bases de données

On valide cette configuration :

Image non disponible
  • en [9] : l'utilisateur jpa a été créé
  • en [10] : on se déconnecte
  • en [11] : on se reconnecte
Image non disponible
  • en [12] : on se connecte en tant qu'utilisateur jpa/jpa
  • en [13] : une fois connecté, l'utilisateur jpa crée une base de données
Image non disponible
  • en [14] : la base s'appellera jpa
  • en [15] : et appartiendra à l'utilisateur jpa
  • en [16] : la base jpa a été créée

XV-A-4. Création de la table [ARTICLES] de la base de données jpa

Nous créons une table [ARTICLES] à partir du script SQL suivant :

Cacher Sélectionnez
  1. /* création table */
  2.
  3. CREATE TABLE ARTICLES (
  4.     ID            INTEGER NOT NULL,
  5.     NOM           VARCHAR(20) NOT NULL,
  6.     PRIX          DOUBLE PRECISION NOT NULL,
  7.     STOCKACTUEL   INTEGER NOT NULL,
  8.     STOCKMINIMUM  INTEGER NOT NULL
  9. );
  10.
  11. INSERT INTO ARTICLES (ID, NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES (1, 'article1', 100, 10, 1);
  12. INSERT INTO ARTICLES (ID, NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES (2, 'article2', 200, 20, 2);
  13. INSERT INTO ARTICLES (ID, NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES (3, 'article3', 300, 30, 3);
  14.
  15. /* contraintes d'intégrité */
  16.
  17. ALTER TABLE ARTICLES ADD CONSTRAINT CHK_ID check (ID>0);
  18. ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRIX check (PRIX>0);
  19. ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKACTUEL check (STOCKACTUEL>0);
  20. ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM>0);
  21. ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NOM check (NOM<>'');
  22. ALTER TABLE ARTICLES ADD CONSTRAINT UNQ_NOM UNIQUE (NOM);
  23.
  24.
  25. /* clé primaire */
  26.
  27. ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
Image non disponible
  • en [1] : on ouvre un script SQL
  • en [2] : on désigne le script SQL
  • en [3] : on doit s'identifier de nouveau (jpa/jpa)
  • en [4] : le script qui va être exécuté
  • en [5] : sélectionner la base dans laquelle le script va être exécuté
  • en [6] : l'exécuter
Image non disponible
  • en [7] : le résultat de l'exécution : la table [ARTICLES] a été créée.
  • en [8] : on demande à voir son contenu
  • en [9] : le contenu de la table.

XV-A-5. Le connecteur ADO.NET de SQL Server Express

Image non disponible

Le connecteur ADO.NET est l'ensemble des classes qui permettent à une application .NET d'utiliser le SGBD SQL Server Express 2005. Les classes du connecteur sont dans l'espace de noms [System.Data], nativement disponible sur toute plate-forme .NET.

XV-B. Le SGBD MySQL5

XV-B-1. Installation

Le SGBD MySQL5 est disponible à l'url [http://dev.mysql.com/downloads/] :

Image non disponible
  • en [1] : choisir la version désirée
  • en [2] : choisir une version Windows
Image non disponible
  • en [3] : choisir la version windows désirée
  • en [4] : le zip téléchargé contient un exécutable [Setup.exe] [4b] qu'il faut extraire et exécuter pour installer MySQL5
Image non disponible
  • en [5] : choisir une installation typique
  • en [6] : une fois l'installation terminée, on peut configurer le serveur MySQL5
Image non disponible
  • en [7] : choisir une configuration standard, celle qui pose le moins de questions
  • en [8] : le serveur MySQL5 sera un service windows
Image non disponible
  • en [9] : par défaut l'administrateur du serveur est root sans mot de passe. On peut garder cette configuration ou donner un nouveau mot de passe à root. Si l'installation de MySQL5 vient derrière une désinstallation d'une version précédente, cette opération peut échouer. Il y a moins moyen d'y revenir.
  • en [10] : on demande la configuration du serveur

L'installation de MySQL5 donne naissance à un dossier dans [Démarrer / Programmes ] :

Image non disponible

On peut utiliser [MySQL Server Instance Config Wizard] pour reconfigurer le serveur :

Image non disponible
Image non disponible
Image non disponible
  • en [3] : nous changeons le mot de passe de root (ici root/root)

XV-B-2. Lancer / Arrêter MySQL5

Le serveur MySQL5 a été installé comme un service windows à démarrage automatique, c.a.d lancé dès le démarrage de windows. Ce mode de fonctionnement est peu pratique. Nous allons le changer :

[Démarrer / Panneau de configuration / Performances et maintenance / Outils d'administration / Services ] :

Image non disponible
  • en [1] : nous double-cliquons sur [Services]
  • en [2] : on voit qu'un service appelé [MySQL] est présent, qu'il est démarré [3] et que son démarrage est automatique [4].

Pour modifier ce fonctionnement, nous double-cliquons sur le service [MySQL] :

Image non disponible
  • en [1] : on met le service en démarrage manuel
  • en [2] : on l'arrête
  • en [3] : on valide la nouvelle configuration du service

Pour lancer et arrêter manuellement le service MySQL, on pourra créer deux raccourcis :

Image non disponible
  • en [1] : le raccourci pour lancer MySQL5
  • en [2] : le raccourci pour l'arrêter

XV-B-3. Clients d'administration MySQL

Sur le site de MySQL, on peut trouver des clients d'administration du SGBD :

Image non disponible
  • en [1] : choisir [MySQL GUI Tools] qui rassemble divers clients graphiques permettant soit d'administrer le SGBD, soit de l'exploiter
  • en [2] : prendre la version Windows qui convient
Image non disponible
  • en [3] : on récupère un fichier .msi à exécuter
  • en [4] : une fois l'installation faite, de nouveaux raccourcis apparaissent dans le dossier [Menu Démarrer / Programmes / mySQL].

Lançons MySQL (via les raccourcis que vous avez créés), puis lançons [MySQL Administrator] via le menu ci-dessus :

Image non disponible
  • en [1] : mettre le mot de passe de l'utilisateur root (root ici)
  • en [2] : on est connecté et on voit que MySQL est actif

XV-B-4. Création d'un utilisateur jpa et d'une base de données jpa

Nous créons maintenant une base de données appelée jpa et un utilisateur de même nom. D'abord l'utilisateur :

Image non disponible
  • en [1] : on sélectionne [User Administration]
  • en [2] : on clique droit dans la partie [User accounts] pour créer un nouvel utilisateur
  • en [3] : l'utilisateur s'appelle jpa et son mot de passe est jpa
  • en [4] : on valide la création
  • en [5] : l'utilisateur [jpa] apparaît dans la fenêtre [User Accounts]

La base de données maintenant :

Image non disponible
  • en [1] : choix de l'option [Catalogs]
  • en [2] : clic droit sur la fenêtre [Schemata] pour créer un nouveau schéma (désigne une base de données)
  • en [3] : on nomme le nouveau schéma
  • en [4] : il apparaît dans la fenêtre [Schemata]
Image non disponible
  • en [5] : on sélectionne le schéma [jpa]
  • en [6] : les objets du schéma [jpa] apparaissent, notamment les tables. Il n'y en a pas encore. Un clic droit permettrait d'en créer. Nous laissons le lecteur le faire.

Revenons à l'utilisateur [jpa] afin de lui donner tous les droits sur le schéma [jpa] :

Image non disponible
  • en [1], puis [2] : on sélectionne l'utilisateur [jpa]
  • en [3] : on sélectionne l'onglet [Schema Privileges]
  • en [4] : on sélectionne le schéma [jpa]
  • en [5] : on va donner à l'utilisateur [jpa] tous les privilèges sur le schéma [jpa]
Image non disponible
  • en [6] : on valide les changements faits

Pour vérifier que l'utilisateur [jpa] peut travailler avec le schéma [jpa], on ferme l'administrateur MySQL. On le relance et on se connecte cette fois sous le nom [jpa/jpa] :

Image non disponible
  • en [1] : on s'identifie (jpa/jpa)
  • en [2] : la connexion a réussi et dans [Schemata], on voit les schémas sur lesquels on a des droits. On voit le schéma [jpa].

Nous allons maintenant créer une table [ARTICLES] à l'aide d'un script SQL.

Image non disponible
  • en [1] : utiliser l'application [MySQL Query Browser]
  • en [2], [3], [4] : s'identifier (jpa / jpa / jpa)
Image non disponible
  • en [5] : ouvrir un script SQL afin de l'exécuter
  • en [6] : désigner le script [schema-articles.sql] suivant :
Cacher Sélectionnez
  1. /******************************************************************************/
  2. /****                                Tables                                ****/
  3. /******************************************************************************/
  4.
  5.
  6.
  7. CREATE TABLE ARTICLES (
  8.     ID            INTEGER NOT NULL,
  9.     NOM           VARCHAR(20) NOT NULL,
  10.     PRIX          DOUBLE PRECISION NOT NULL,
  11.     STOCKACTUEL   INTEGER NOT NULL,
  12.     STOCKMINIMUM  INTEGER NOT NULL
  13. );
  14.
  15. INSERT INTO ARTICLES (ID, NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES (1,'article1', 100, 10, 1);
  16. INSERT INTO ARTICLES (ID, NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES (2,'article2', 200, 20, 2);
  17. INSERT INTO ARTICLES (ID, NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES (3,'article3', 300, 30, 3);
  18.
  19. COMMIT WORK;
  20.
  21.
  22.
  23. /* Check constraints definition */
  24.
  25. ALTER TABLE ARTICLES ADD CONSTRAINT CHK_ID check (ID>0);
  26. ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRIX check (PRIX>0);
  27. ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKACTUEL check (STOCKACTUEL>0);
  28. ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM>0);
  29. ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NOM check (NOM<>'');
  30.
  31.
  32. /******************************************************************************/
  33. /****                          Unique Constraints                          ****/
  34. /******************************************************************************/
  35.
  36. ALTER TABLE ARTICLES ADD CONSTRAINT UNQ_NOM UNIQUE (NOM);
  37.
  38.
  39. /******************************************************************************/
  40. /****                             Primary Keys                             ****/
  41. /******************************************************************************/
  42.
  43. ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
Image non disponible
  • en [7] : le script chargé
  • en [8] : on l'exécute
  • en [9] : la table [ARTICLES] a été créée

XV-B-5. Installation du connecteur ADO.NET de MySQL5

Le connecteur ADO.NET de MySQL5 est disponible (avril 2008) à l'adresse [http://dev.mysql.com/downloads/connector/net/5.1.html] :

Image non disponible

L'installation de ce connecteur ajoute un espace de noms à la plate-forme .NET :

XV-B-6. Installation du pilote ODBC de MySQL5

Le connecteur ODBC (Open DataBase Connectivity) de MySQL5 est disponible (avril 2008) à l'adresse [http://dev.mysql.com/downloads/connector/odbc/3.51.html] :

Image non disponible

Après installation, la présence du connecteur ODBC peut être vérifiée de la façon suivante :

Image non disponible
  • en [1], sélectionner [Outils d'administration] (sur XP Pro : Menu Démarrer / Panneau de configuration / Performances et maintenance / Outils d'administration)
  • en [2], double-cliquer sur [Sources de données (ODBC)]
  • en [3], sélectionner l'onglet [Pilotes ODBC]
  • en [4], le pilote ODBC de MySQL


C# Language Reference

C# Language Reference

[This topic is part of a preview and is subject to change in future releases. Blank topics are included as placeholders.]

Version: 0.20

Authors: Anders Hejlsberg and Scott Wiltamuth

© 1987-2000 Microsoft Corporation. All rights reserved.


1. Introduction

C# is a simple, modern, object oriented, and type-safe programming language derived from C and C++. C# (pronounced “C sharp”) is firmly planted in the C and C++ family tree of languages, and will immediately be familiar to C and C++ programmers. C# aims to combine the high productivity of Visual Basic and the raw power of C++.

C# is provided as part of Microsoft Visual Studio 7.0. In addition to C#, Visual Studio supports Visual Basic, Visual C++, and the scripting languages VBScript and JScript. All of these languages provide access to the Microsoft .NET platform, which includes a common execution engine and a rich class library. The Microsoft .NET platform defines a "Common Language Subset" (CLS), a sort of lingua franca that ensures seamless interoperability between CLS-compliant languages and class libraries. For C# developers, this means that even though C# is a new language, it has complete access to the same rich class libraries that are used by seasoned tools such as Visual Basic and Visual C++. C# itself does not include a class library.

The rest of this chapter describes the essential features of the language. While later chapters describe rules and exceptions in a detail-oriented and sometimes mathematical manner, this chapter strives for clarity and brevity at the expense of completeness. The intent is to provide the reader with an introduction to the language that will facilitate the writing of early programs and the reading of later chapters.


1.1 Hello, world

The canonical “Hello, world” program can be written as follows:

using System;
class Hello
{
   static void Main() {
      Console.WriteLine("Hello, world");
   }
}

The source code for a C# program is typically stored in one or more text files with a file extension of .cs, as in hello.cs. Using the command-line compiler provided with Visual Studio, such a program can be compiled with the command line directive

csc hello.cs

which produces an executable program named hello.exe. The output of the program is:

Hello, world

Close examination of this program is illuminating:

  • The using System; directive references a namespace called System that is provided by the Microsoft .NET class library. This namespace contains the Console class referred to in the Main method. Namespaces provide a hierarchical means of organizing the elements of a class library. A “using” directive enables unqualified use of the types that are members of the namespace. The “Hello, world” program uses Console.WriteLine as a shorthand for System.Console.WriteLine.
  • The Main method is a member of the class Hello. It has the static modifier, and so it is a method on the class Hello rather than on instances of this class.
  • The main entry point for a program – the method that is called to begin execution – is always a static method named Main.
  • The “Hello, world” output is produced through the use of a class library. The language does not itself provide a class library. Instead, it uses a common class library that is also used by languages such as Visual Basic and Visual C++.

For C and C++ developers, it is interesting to note a few things that do not appear in the “Hello, world” program.

  • The program does not use a global method for Main. Methods and variables are not supported at the global level; such elements are always contained within type declarations (e.g., class and struct declarations).
  • The program does not use either “::” or “->” operators. The “::” is not an operator at all, and the “->” operator is used in only a small fraction of programs. The separator “.” is used in compound names such as Console.WriteLine.
  • The program does not contain forward declarations. Forward declarations are never needed, as declaration order is not significant.
  • The program does not use #include to import program text. Dependencies among programs are handled symbolically rather than textually. This system eliminates barriers between programs written in different languages. For example, the Console class could be written in another language.


1.2 Types

C# supports two kinds of types: value types and reference types. Value types include simple types (e.g., char, int, and float), enum types, and struct types. Reference types include class types, interface types, delegate types, and array types.

Value types differ from reference types in that variables of the value types directly contain their data, whereas variables of the reference types store references to objects. With reference types, it is possible for two variables to reference the same object, and thus possible for operations on one variable to affect the object referenced by the other variable. With value types, the variables each have their own copy of the data, and it is not possible for operations on one to affect the other.

The example

using System;
class Class1
{
   public int Value = 0;
}
class Test
{
   static void Main() {
      int val1 = 0;
      int val2 = val1;
      val2 = 123;
      Class1 ref1 = new Class1();
      Class1 ref2 = ref1;
      ref2.Value = 123;
      Console.WriteLine("Values: {0}, {1}", val1, val2);
      Console.WriteLine("Refs: {0}, {1}", ref1.Value, ref2.Value);
   }
}

shows this difference. The output of the program is

Values: 0, 123
Refs: 123, 123

The assignment to the local variable val1 does not impact the local variable val2 because both local variables are of a value type (the type int) and each local variable of a value type has its own storage. In contrast, the assignment ref2.Value = 123; affects the object that both ref1 and ref2 reference.

The lines

Console.WriteLine("Values: {0}, {1}", val1, val2);
Console.WriteLine("Refs: {0}, {1}", ref1.Value, ref2.Value);

deserve further comment, as they demonstrate some of the string formatting behavior of Console.WriteLine, which in fact takes a variable number of arguments. The first argument is a string, which may contain numbered placeholders like {0} and {1}. Each placeholder refers to a trailing argument with {0} referring to the second argument, {1} referring to the third argument, and so on. Before the output is sent to the console, each placeholder is replaced with the formatted value of its corresponding argument.

Developers can define new value types through enum and struct declarations, and can define new reference types via class, interface, and delegate declarations. The example

using System;
public enum Color
{
   Red, Blue, Green
}
public struct Point
{
   public int x, y;
}
public interface IBase
{
   void F();
}
public interface IDerived: IBase
{
   void G();
}
public class A
{
   protected virtual void H() {
      Console.WriteLine("A.H");
   }
}
public class B: A, IDerived
{
   public void F() {
      Console.WriteLine("B.F, implementation of IDerived.F");
   }
   public void G() {
      Console.WriteLine("B.G, implementation of IDerived.G");
   }
   override protected void H() {
      Console.WriteLine("B.H, override of A.H");
   }
}
public delegate void EmptyDelegate();

shows an example or two for each kind of type declaration. Later sections describe type declarations in greater detail.


1.2.1 Predefined types

C# provides a set of predefined types, most of which will be familiar to C and C++ developers.

The predefined reference types are object and string. The type object is the ultimate base type of all other types, and the type string is used to represent Unicode string values.

The predefined value types include signed and unsigned integral types, floating point types, and the types bool, char, and decimal. The signed integral types are sbyte, short, int, and long; the unsigned integral types are byte, ushort, uint, and ulong; and the floating point types are float and double.

The bool type is used to represent boolean values: values that are either true or false. The inclusion of bool makes it easier to write self-documenting code, and also helps eliminate the all-too-common C++ coding error in which a developer mistakenly uses “=” when “==” should have been used. In C#, the example

int i = ...;
F(i);
if (i = 0)  // Bug: the test should be (i == 0)
  G();

is invalid because the expression i = 0 is of type int, and if statements require an expression of type bool.

The char type is used to represent Unicode characters. A variable of type char represents a single 16-bit Unicode character.

The decimal type is appropriate for calculations in which rounding errors are unacceptable. Common examples include financial calculations such as tax computations and currency conversions. The decimal type provides 28 significant digits.

The table below lists the predefined types, and shows how to write literal values for each of them.

Type | Description | Example
object | The ultimate base type of all other types | object o = null;
string | String type; a string is a sequence of Unicode characters | string s = "Hello";
sbyte | 8-bit signed integral type | sbyte val = 12;
short | 16-bit signed integral type | short val = 12;
int | 32-bit signed integral type | int val = 12;
long 64-bit signed integral type long val1 = 12;
long val2 = 34L;
byte 8-bit unsigned integral type byte val1 = 12;
byte val2 = 34U;
ushort 16-bit unsigned integral type ushort val1 = 12;
ushort val2 = 34U;
uint 32-bit unsigned integral type uint val1 = 12;
uint val2 = 34U;
ulong 64-bit unsigned integral type ulong val1 = 12;
ulong val2 = 34U;
ulong val3 = 56L;
ulong val4 = 78UL;
float | Single-precision floating point type | float val = 1.23F;
double Double-precision floating point type double val1 = 1.23;
double val2 = 4.56D;
bool Boolean type; a bool value is either true or false bool val1 = true;
bool val2 = false;
char | Character type; a char value is a Unicode character | char val = 'h';
decimal | Precise decimal type with 28 significant digits | decimal val = 1.23M;

Each of the predefined types is shorthand for a system-provided type. For example, the keyword int is shorthand for a struct named System.Int32. It is considered good style to use the keyword rather than the complete system type name.

Predefined value types such as int are treated specially in a few ways but are for the most part treated exactly like other structs. Operator overloading enables developers to define types that behave like the predefined value types. For instance, a Digit struct can support the same mathematical operations as the predefined integral types, and can define conversions between Digit and predefined types.

The predefined types employ operator overloading themselves. For example, the comparison operators == and != have different semantics for different predefined types:

  • Two expressions of type int are considered equal if they represent the same integer value.
  • Two expressions of type object are considered equal if both refer to the same object, or if both are null.
  • Two expressions of type string are considered equal if the string instances have identical lengths and identical characters in each character position, or if both are null.

The example

class Test
{
   static void Main() {
      string s = "Test";
      string t = string.Copy(s);
      Console.WriteLine(s == t);
      Console.WriteLine((object)s == (object)t);
   }
}

produces the output

True
False

because the first comparison compares two expressions of type string, and the second comparison compares two expressions of type object.


1.2.2 Conversions

There are two kinds of conversions: implicit conversions and explicit conversions. Implicit conversions are supplied for conversions that can safely be performed without careful scrutiny. For instance, the conversion from int to long is an implicit conversion. This conversion always succeeds, and never results in a loss of information. Implicit conversions can be performed implicitly, as shown in the example

using System;
class Test
{
   static void Main() {
      int intValue = 123;
      long longValue = intValue;
      Console.WriteLine("(long) {0} = {1}", intValue, longValue);
   }
}

which implicitly converts an int to a long.

In contrast, explicit conversions are performed with a cast expression. The example

using System;
class Test
{
   static void Main() {
      long longValue = Int64.MaxValue;
      int intValue = (int) longValue;
      Console.WriteLine("(int) {0} = {1}", longValue, intValue);
   }
}

uses an explicit conversion to convert a long to an int. The output is:

(int) 9223372036854775807 = -1

because an overflow occurs.


1.2.3 Array types

Arrays may be single-dimensional or multi-dimensional. Both “rectangular” and “jagged” arrays are supported.

Single-dimensional arrays are the most common type, so this is a good starting point. The example

using System;
class Test
{
   static void Main() {
      int[] arr = new int[5];
      for (int i = 0; i < arr.Length; i++)
         arr[i] = i * i;
      for (int i = 0; i < arr.Length; i++)
         Console.WriteLine("arr[{0}] = {1}", i, arr[i]);
   }
}

creates a single-dimensional array of int values, initializes the array elements, and then prints each of them out. The program output is:

arr[0] = 0
arr[1] = 1
arr[2] = 4
arr[3] = 9
arr[4] = 16

The type int[] used in the previous example is an array type. Array types are written using a non-array-type followed by one or more rank specifiers. The example

class Test
{
   static void Main() {
      int[] a1;         // single-dimensional array of int
      int[,] a2;         // 2-dimensional array of int
      int[,,] a3;         // 3-dimensional array of int
      int[][] j2;         // "jagged" array: array of (array of int)
      int[][][] j3;      // array of (array of (array of int))
   }
}

shows a variety of local variable declarations that use array types with int as the element type.

Arrays are reference types, and so the declaration of an array variable merely sets aside space for the reference to the array. Array instances are actually created via array initializers and array creation expressions. The example

class Test
{
   static void Main() {
      int[] a1 = new int[] {1, 2, 3};
      int[,] a2 = new int[,] {{1, 2, 3}, {4, 5, 6}};
      int[,,] a3 = new int[10, 20, 30];
      int[][] j2 = new int[3][];
      j2[0] = new int[] {1, 2, 3};
      j2[1] = new int[] {1, 2, 3, 4, 5, 6};
      j2[2] = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9};
   }
}

shows a variety of array creation expressions. The variables a1, a2 and a3 denote rectangular arrays, and the variable j2 denotes a jagged array. It should be no surprise that these terms are based on the shapes of the arrays. Rectangular arrays always have a rectangular shape. Given the length of each dimension of the array, its rectangular shape is clear. For example, the length of a3’s three dimensions are 10, 20, and 30 respectively, and it is easy to see that this array contains 10*20*30 elements.

In contrast, the variable j2 denotes a “jagged” array, or an “array of arrays”. Specifically, j2 denotes an array of an array of int, or a single-dimensional array of type int[]. Each of these int[] variables can be initialized individually, and this allows the array to take on a jagged shape. The example gives each of the int[] arrays a different length. Specifically, the length of j2[0] is 3, the length of j2[1] is 6, and the length of j2[2] is 9.

The element type and number of dimensions are part of an array’s type, but the length of each dimension is not. This split is made clear in the language syntax, as the length of each dimension is specified in the array creation expression rather than in the array type. For instance the declaration

int[,,] a3 = new int[10, 20, 30];

has an array type of int[,,] and an array creation expression of new int[10, 20, 30].

For local variable and field declarations, a shorthand form is permitted so that it is not necessary to re-state the array type. For instance, the example

int[] a1 = new int[] {1, 2, 3};

can be shortened to

int[] a1 = {1, 2, 3};

without any change in program semantics.

The context in which an array initializer such as {1, 2, 3} is used determines the type of the array being initialized. The example

class Test
{
   static void Main() {
      short[] a = {1, 2, 3};
      int[]   b = {1, 2, 3};
      long[] c = {1, 2, 3};
   }
}

shows that the same array initializer can be used for several different array types. Because context is required to determine the type of an array initializer, it is not possible to use an array initializer in an expression context.


1.2.4 Type system unification

C# provides a “unified type system”. All types – including value types – derive from the type object. It is possible to call object methods on any value, even values of “primitive” types such as int. The example

using System;
class Test
{
   static void Main() {
      Console.WriteLine(3.ToString());
   }
}

calls the object-defined ToString method on an integer literal.

The example

class Test
{
   static void Main() {
      int i = 123;
      object o = i;      // boxing
      int j = (int) o;   // unboxing
   }
}

is more interesting. An int value can be converted to object and back again to int. This example shows both boxing and unboxing. When a variable of a value type needs to be converted to a reference type, an object box is allocated to hold the value, and the value is copied into the box. Unboxing is just the opposite. When an object box is cast back to its original value type, the value is copied out of the box and into the appropriate storage location.

This type system unification provides value types with the benefits of object-ness without introducing unnecessary overhead. For programs that don’t need int values to act like objects, int values are simply 32-bit values. For programs that need int values to behave like objects, this capability is available on-demand. This ability to treat value types as objects bridges the gap between value types and reference types that exists in most languages. For example, a Stack class can provide Push and Pop methods that take and return object values.

public class Stack
{
   public object Pop() {...}
   public void Push(object o) {...}
}

Because C# has a unified type system, the Stack class can be used for stacks of any type, including value types like int.


1.3 Variables and parameters

Variables represent storage locations. Every variable has a type that determines what values can be stored in the variable. Local variables are variables that are declared in methods, properties, or indexers. A local variable is usually defined by specifying a type name and a declarator that specifies the variable name and an optional initial value, as in:

int a;
int b = 1;

but it is also possible for a local variable declaration to include multiple declarators. The declarations of a and b can be rewritten as:

int a, b = 1;

A variable must be definitely assigned (§5.3) before its value can be obtained. The example

class Test
{
   static void Main() {
      int a;
      int b = 1;
      int c = a + b;
      ...
   }
}

is invalid because it attempts to use the variable a before it is assigned a value.

A field (§10.4) is a variable that is associated with a class or struct, or an instance of a class or struct. A field declared with the static modifier defines a static variable, and a field declared without this modifier defines an instance variable. The example

using System.Data;
class Employee
{
   private static DataSet ds;
   public string Name;
   public decimal Salary;
   ...
}

shows an Employee class that has a private static variable and two public instance variables.

Formal parameter declarations also define variables. There are four kinds of parameters: value parameters, reference parameters, output parameters, and parameter arrays.

A value parameter is used for “in” parameter passing, in which the value of an argument is passed into a method, and modifications of the parameter do not impact the original argument. A value parameter refers to its own memory location, one that is distinct from the memory location of the corresponding argument. This memory location is initialized by copying the value of the corresponding argument. The example

using System;
class Test {
   static void F(int p) {
      Console.WriteLine("p = {0}", p);
      p++;
   }
   static void Main() {
      int a = 1;
      Console.WriteLine("pre: a = {0}", a);
      F(a);
      Console.WriteLine("post: a = {0}", a);
   }
}

shows a method F that has a value parameter named p. The example produces the output:

pre: a = 1
p = 1
post: a = 1

even though the value parameter p is modified.

A reference parameter is used for “by reference” parameter passing, in which the parameter acts as an alias for a caller-provided argument. A reference parameter does not itself define a memory location, but rather refers to the memory location of the corresponding argument. Modifications of a reference parameter directly and immediately impact the corresponding argument. A reference parameter is declared with a ref modifier. The example

using System;
class Test {
   static void Swap(ref int a, ref int b) {
      int t = a;
      a = b;
      b = t;
   }
   static void Main() {
      int x = 1;
      int y = 2;

      Console.WriteLine("pre: x = {0}, y = {1}", x, y);
      Swap(ref x, ref y);
      Console.WriteLine("post: x = {0}, y = {1}", x, y);
   }
}

shows a Swap method that has two reference parameters. The output of the program is:

pre: x = 1, y = 2
post: x = 2, y = 1

The ref keyword must be used in both the declaration of the formal parameter and in uses of it. The use of ref at the call site calls special attention to the parameter so that a developer reading the code will understand that the argument could change as a result of the call.

An output parameter is similar to a reference parameter, except that the initial value of the caller-provided argument is unimportant. An output parameter is declared with an out modifier. The example

using System;
class Test {
   static void Divide(int a, int b, out int result, out int remainder) {
      result = a / b;
      remainder = a % b;
   }
   static void Main() {
      for (int i = 1; i < 10; i++)
         for (int j = 1; j < 10; j++) {
            int ans, r;
            Divide(i, j, out ans, out r);
            Console.WriteLine("{0} / {1} = {2}r{3}", i, j, ans, r);
         }
   }
}

shows a Divide method that includes two output parameters – one for the result of the division and another for the remainder.

For value, reference, and output parameters, there is a one-to-one correspondence between caller-provided arguments and the parameters used to represent them. A parameter array enables a many-to-one relationship: many arguments can be represented by a single parameter array. In other words, parameter arrays enable variable length argument lists.

A parameter array is declared with a params modifier. There can be only one parameter array for a given method, and it is always the last parameter specified. The type of a parameter array is always a single dimensional array type. A caller can either pass a single argument of this array type, or any number of arguments of the element type of this array type. For instance, the example

using System;
class Test
{
   static void F(params int[] args) {
      Console.WriteLine("# of arguments: {0}", args.Length);
      for (int i = 0; i < args.Length; i++)
         Console.WriteLine("\targs[{0}] = {1}", i, args[i]);
   }
   static void Main() {
      F();
      F(1);
      F(1, 2);
      F(1, 2, 3);
      F(new int[] {1, 2, 3, 4});
   }
}

shows a method F that takes a variable number of int arguments, and several invocations of this method. The output is:

# of arguments: 0
# of arguments: 1
   args[0] = 1
# of arguments: 2
   args[0] = 1
   args[1] = 2
# of arguments: 3
   args[0] = 1
   args[1] = 2
   args[2] = 3
# of arguments: 4
   args[0] = 1
   args[1] = 2
   args[2] = 3
   args[3] = 4

Most of the examples presented in this introduction use the WriteLine method of the Console class. The argument substitution behavior of this method, as exhibited in the example

int a = 1, b = 2;
Console.WriteLine("a = {0}, b = {1}", a, b);

is accomplished using a parameter array. The WriteLine method provides several overloaded methods for the common cases in which a small number of arguments are passed, and one method that uses a parameter array.

namespace System
{
   public class Console
   {
      public static void WriteLine(string s) {...}
      public static void WriteLine(string s, object a) {...}
      public static void WriteLine(string s, object a, object b) {...}
      ...
      public static void WriteLine(string s, params object[] args) {...}
   }
}


1.4 Automatic memory management

Manual memory management requires developers to manage the allocation and de-allocation of blocks of memory. Manual memory management is both time consuming and difficult. Automatic memory management is provided so that developers are freed from this burdensome task. In the vast majority of cases, automatic memory management increases code quality and enhances developer productivity without negatively impacting either expressiveness or performance.

The example

using System;
public class Stack
{
   private Node first = null;
   public bool Empty {
      get {
         return (first == null);
      }
   }
   public object Pop() {
      if (first == null)
         throw new Exception("Can't Pop from an empty Stack.");
      else {
         object temp = first.Value;
         first = first.Next;
         return temp;
      }
   }
   public void Push(object o) {
      first = new Node(o, first);
   }
   class Node
   {
      public Node Next;
      public object Value;
      public Node(object value): this(value, null) {}
      public Node(object value, Node next) {
         Next = next;
         Value = value;
      }
   }
}

shows a Stack class implemented as a linked list of Node instances. Node instances are created in the Push method and are garbage collected when no longer needed. A Node instance becomes eligible for garbage collection when it is no longer possible for any code to access it. For instance, when an item is removed from the Stack, the associated Node instance becomes eligible for garbage collection.

The example

class Test
{
   static void Main() {
      Stack s = new Stack();
      for (int i = 0; i < 10; i++)
         s.Push(i);
      s = null;
   }
}

shows a test program that uses the Stack class. A Stack is created and initialized with 10 elements, and then assigned the value null. Once the variable s is assigned null, the Stack and the associated 10 Node instances become eligible for garbage collection. The garbage collector is permitted to clean up immediately, but is not required to do so.

For developers who are generally content with automatic memory management but sometimes need fine-grained control or that extra iota of performance, C# provides the ability to write “unsafe” code. Such code can deal directly with pointer types, and fix objects to temporarily prevent the garbage collector from moving them. This “unsafe” code feature is in fact a “safe” feature from the perspective of both developers and users. Unsafe code must be clearly marked in the code with the modifier unsafe, so developers can't possibly use unsafe features accidentally, and the compiler and the execution engine work together to ensure that unsafe code cannot masquerade as safe code.

The example

using System;
class Test
{
   unsafe static void WriteLocations(byte[] arr) {
      fixed (byte *p_arr = arr) {
         byte *p_elem = p_arr;
         for (int i = 0; i < arr.Length; i++) {
            byte value = *p_elem;
            string addr = int.Format((int) p_elem, "X");
            Console.WriteLine("arr[{0}] at 0x{1} is {2}", i, addr, value);
            p_elem++;
         }
      }
   }
   static void Main() {
      byte[] arr = new byte[] {1, 2, 3, 4, 5};
      WriteLocations(arr);
   }
}

shows an unsafe method named WriteLocations that fixes an array instance and uses pointer manipulation to iterate over the elements. The index, value, and location of each array element is written to the console. One possible output of the program is:

arr[0] at 0x8E0360 is 1
arr[1] at 0x8E0361 is 2
arr[2] at 0x8E0362 is 3
arr[3] at 0x8E0363 is 4
arr[4] at 0x8E0364 is 5

but of course the exact memory locations are subject to change.


1.5 Expressions

C# includes unary operators, binary operators, and one ternary operator. The following table summarizes the operators, listing them in order of precedence from highest to lowest:

Section | Category | Operators
7.5 | Primary | (x) x.y f(x) a[x] x++ x-- new typeof sizeof checked unchecked
7.6 | Unary | + - ! ~ ++x --x (T)x
7.7 | Multiplicative | * / %
7.7 | Additive | + -
0 | Shift | << >>
7.9 | Relational | < > <= >= is
7.9 | Equality | == !=
7.10 | Logical AND | &
7.10 | Logical XOR | ^
7.10 | Logical OR | |
7.11 | Conditional AND | &&
7.11 | Conditional OR | ||
7.12 | Conditional | ?:
7.13 | Assignment | = *= /= %= += -= <<= >>= &= ^= |=

When an expression contains multiple operators, the precedence of the operators controls the order in which the individual operators are evaluated. For example, the expression x + y * z is evaluated as x + (y * z) because the * operator has higher precedence than the + operator.

When an operand occurs between two operators with the same precedence, the associativity of the operators controls the order in which the operations are performed:

  • Except for the assignment operators, all binary operators are left-associative, meaning that operations are performed from left to right. For example, x + y + z is evaluated as (x + y) + z.
  • The assignment operators and the conditional operator (?:) are right-associative, meaning that operations are performed from right to left. For example, x = y = z is evaluated as x = (y = z).

Precedence and associativity can be controlled using parentheses. For example, x + y * z first multiplies y by z and then adds the result to x, but (x + y) * z first adds x and y and then multiplies the result by z.


1.6 Statements

C# borrows most of its statements directly from C and C++, though there are some noteworthy additions and modifications. The table below lists the kinds of statements that can be used, and provides an example for each.

Statement Example
Statement lists and block statements static void Main() {
F();
G();
{
H();
I();
}
}
Labeled statements and goto statements static void Main(string[] args) {
if (args.Length == 0)
goto done:
Console.WriteLine(args.Length);

done:
Console.WriteLine("Done");
}
Local constant declarations static void Main() {
const float pi = 3.14;
const int r = 123;
Console.WriteLine(pi * r * r);
}
Local variable declarations static void Main() {
int a;
int b = 2, c = 3;
a = 1;
Console.WriteLine(a + b + c);
}
Expression statements static int F(int a, int b) {
return a + b;
}

static void Main() {
F(1, 2); // Expression statement
}
if statements static void Main(string[] args) {
if (args.Length == 0)
Console.WriteLine("No args");
else
Console.WriteLine("Args");
}
switch statements static void Main(string[] args) {
switch (args.Length) {
case 0:
Console.WriteLine("No args");
break;
case 1:
Console.WriteLine("One arg ");
break;
default:
int n = args.Length;
Console.WriteLine("{0} args", n);
break;
}
}
while statements static void Main(string[] args) {
int i = 0;
while (i < args.length) {
Console.WriteLine(args[i]);
i++;
}
}
do statements static void Main() {
string s;
do { s = Console.ReadLine(); }
while (s != "Exit");
}
for statements static void Main(string[] args) {
for (int i = 0; i < args.length; i++)
Console.WriteLine(args[i]);
}
foreach statements static void Main(string[] args) {
foreach (string s in args)
Console.WriteLine(s);
}
break statements static void Main(string[] args) {
int i = 0;
while (true) {
if (i > args.Length)
break;
Console.WriteLine(args[i++]);
}
}
continue statements static void Main(string[] args) {
int i = 0;
while (true) {
Console.WriteLine(args[i++]);
if (i > args.Length)
continue;
break;
}
}
return statements static int F(int a, int b) {
return a + b;
}

static void Main() {
Console.WriteLine(F(1, 2));
return;
}
throw statements and try statements static int F(int a, int b) {
if (b == 0)
throw new Exception("Divide by zero");
return a / b;
}

static void Main() {
try {
Console.WriteLine(F(5, 0));
}
catch(Exception e) {
Console.WriteLine("Error");
}
}
checked and unchecked statements static void Main() {
int x = 100000, y = 100000;
Console.WriteLine(unchecked(x * y));
Console.WriteLine(checked(x * y)); // Error
Console.WriteLine(x * y); // Error
}
lock statements static void Main() {
A a = ...
lock(a) {
a.P = a.P + 1;
}
}
using statements using System;

class Resource: IDisposable
{
public void F() {
Console.WriteLine("Resource.F");
}

public void Dispose() {...}

...
}

static void Main() {
using(Resource r = new Resource()) {
r.F();
}
}


1.7 Classes

Class declarations define new reference types. A class can inherit from another class, and can implement zero or more interfaces.

Class members can include constants, fields, methods, properties, indexers, events, operators, constructors, destructors, and nested type declarations. Each member has an associated accessibility, which controls the regions of program text that are able to access the member. There are five possible forms of accessibility. These are summarized in the table below.

Form | Intuitive meaning
public | Access not limited
protected | Access limited to the containing class or types derived from the containing class
internal | Access limited to this program
protected internal | Access limited to this program or types derived from the containing class
private | Access limited to the containing type

The example

using System;
class MyClass
{
   public MyClass() {
      Console.WriteLine("Constructor");
   }
   public MyClass(int value) {
      MyField = value;
      Console.WriteLine("Constructor");
   }
   ~MyClass() {
      Console.WriteLine("Destructor");
   }
   public const int MyConst = 12;
   public int MyField = 34;
   public void MyMethod(){
      Console.WriteLine("MyClass.MyMethod");
   }
   public int MyProperty {
      get {
         return MyField;
      }
      set {
         MyField = value;
      }
   }
   public int this[int index] {
      get {
         return 0;
      }
      set {
         Console.WriteLine("this[{0}] = {1}", index, value);
      }
   }
   public event EventHandler MyEvent;
   public static MyClass operator+(MyClass a, MyClass b) {
      return new MyClass(a.MyField + b.MyField);
   }
   internal class MyNestedClass
   {}
}

shows a class that contains each kind of member. The example

class Test
{
   static void Main() {
      // Constructor usage
      MyClass a = new MyClass();
      MyClass b = new MyClass(123);
      // Constant usage
      Console.WriteLine("MyConst = {0}", MyClass.MyConst);
      // Field usage
      a.MyField++;
      Console.WriteLine("a.MyField = {0}", a.MyField);
      // Method usage
      a.MyMethod();
      // Property usage
      a.MyProperty++;
      Console.WriteLine("a.MyProperty = {0}", a.MyProperty);
      // Indexer usage
      a[3] = a[1] = a[2];
      Console.WriteLine("a[3] = {0}", a[3]);
      // Event usage
      a.MyEvent += new EventHandler(MyHandler);
      // Overloaded operator usage
      MyClass c = a + b;
   }
   static void MyHandler(object sender, EventArgs e) {
      Console.WriteLine("Test.MyHandler");
   }
   internal class MyNestedClass
   {}
}

shows uses of these members.


1.7.1 Constants

A constant is a class member that represents a constant value: a value that can be computed at compile-time. Constants are permitted to depend on other constants within the same program as long as there are no circular dependencies. The example

class Constants
{
   public const int A = 1;
   public const int B = A + 1;
}

shows a class named Constants that has two public constants.

Constants are implicitly static, and can be accessed through the class, as in

class Test
{
   static void Main() {
      Console.WriteLine("{0}, {1}", Constants.A, Constants.B);
   }
}

which prints out the values of Constants.A and Constants.B.


1.7.2 Fields

A field is a member that represents a variable associated with an object or class. The example

class Color
{
   internal ushort redPart;
   internal ushort bluePart;
   internal ushort greenPart;
   public Color(ushort red, ushort blue, ushort green) {
      redPart = red;
      bluePart = blue;
      greenPart = green;
   }
   ...
}

shows a Color class that has internal instance fields named redPart, greenPart, and bluePart. Fields can also be static, as shown in the example

class Color
{
   public static Color Red = new Color(0xFF, 0, 0);
   public static Color Blue = new Color(0, 0xFF, 0);
   public static Color Green = new Color(0, 0, 0xFF);
   public static Color White = new Color(0xFF, 0xFF, 0xFF);
   ...
}

which shows static fields for Red, Blue, Green, and White.

Static fields are not a perfect match for this scenario. The fields are initialized when the Color class is loaded, but after this initialization there is nothing to stop a client from changing them. Such a modification could cause unpredictable errors in other programs that use Color and assume that the values do not change. Readonly fields can be used to prevent such problems. Assignments to a readonly field can only occur as part of the declaration, or in a constructor in the same class. Thus, the Color class can be enhanced by adding the readonly modifier for the static fields:

class Color
{
   internal ushort redPart;
   internal ushort bluePart;
   internal ushort greenPart;
   public Color(ushort red, ushort blue, ushort green) {
      redPart = red;
      bluePart = blue;
      greenPart = green;
   }
   public static readonly Color Red = new Color(0xFF, 0, 0);
   public static readonly Color Blue = new Color(0, 0xFF, 0);
   public static readonly Color Green = new Color(0, 0, 0xFF);
   public static readonly Color White = new Color(0xFF, 0xFF, 0xFF);
}


1.7.3 Methods

A method is a member that implements a computation or action that can be performed by an object or class. Methods have a list of formal parameters (which may be empty), a return value (or void), and are either static or non-static. Static methods are accessed through the class. Non-static methods, which are also called instance methods, are accessed through instances of the class. The example

using System;
public class Stack
{
   public static Stack Clone(Stack s) {...}
   public static Stack Flip(Stack s) {...}
   public object Pop() {...}
   public void Push(object o) {...}
   public override string ToString() {...}
   ...
}
class Test
{
   static void Main() {
      Stack s = new Stack();
      for (int i = 1; i < 10; i++)
         s.Push(i);
      Stack flipped = Stack.Flip(s);
      Stack cloned = Stack.Clone(s);
      Console.WriteLine("Original stack: " + s.ToString());
      Console.WriteLine("Flipped stack: " + flipped.ToString());
      Console.WriteLine("Cloned stack: " + cloned.ToString());
   }
}

shows a Stack that has several static methods (Clone and Flip) and several instance methods (Push, Pop, and ToString).

Methods can be overloaded, which means that multiple methods may have the same name so long as they have unique signatures. The signature of a method consists of the name of the method and the number, modifiers, and types of its formal parameters. The signature of a method specifically does not include the return type. The example

class Test
{
   static void F() {
      Console.WriteLine("F()");
   }
   static void F(object o) {
      Console.WriteLine("F(object)");
   }
   static void F(int value) {
      Console.WriteLine("F(int)");
   }
   static void F(int a, int b) {
      Console.WriteLine("F(int, int)");
   }
   static void F(int[] values) {
      Console.WriteLine("F(int[])");
   }
   static void Main() {
      F();
      F(1);
      F((object)1);
      F(1, 2);
      F(new int[] {1, 2, 3});
   }
}

shows a class with a number of F methods. The output of the program is

F()
F(int)
F(object)
F(int, int)
F(int[])


1.7.4 Properties

A property is a member that provides access to an attribute of an object or a class. Examples of properties include the length of a string, the size of a font, the caption of a window, the name of a customer, and so on. Properties are a natural extension of fields. Both are named members with associated types, and the syntax for accessing fields and properties is the same. However, unlike fields, properties do not denote storage locations. Instead, properties have accessors that specify the statements to execute in order to read or write their values.

Properties are defined with property declarations. The first part of a property declaration looks quite similar to a field declaration. The second part includes a get accessor and/or a set accessor. In the example below, the Button class defines a Caption property.

public class Button
{
   private string caption;
   public string Caption {
      get {
         return caption;
      }
      set {
         caption = value;
         Repaint();
      }
   }
}

Properties that can be both read and written, like the Caption property, include both get and set accessors. The get accessor is called when the property’s value is read; the set accessor is called when the property’s value is written. In a set accessor, the new value for the property is given in an implicit parameter named value.

Declaration of properties is relatively straightforward, but the true value of properties shows itself in use rather than in declaration. The Caption property can be read and written in the same way that fields can be read and written:

Button b = new Button();
b.Caption = "ABC";      // set
string s = b.Caption;   // get
b.Caption += "DEF”;      // get & set


1.7.5 Events

An event is a member that enables an object or class to provide notifications. A class defines an event by providing an event declaration, which looks quite similar to a field or property declaration but with an added event keyword. The type of this declaration must be a delegate type.

In the example

public delegate void EventHandler(object sender, Event e);
public class Button
{
   public event EventHandler Click;
   public void Reset() {
      Click = null;
   }
}

the Button class defines a Click event of type EventHandler. Inside the Button class, the Click member corresponds exactly to a private field of type EventHandler. However, outside the Button class, the Click member can only be used on the left hand side of the += and -= operators. This restricts client code to adding or removing an event handler. The example

using System;
public class Form1
{
   public Form1() {
      // Add Button1_Click as an event handler for Button1’s Click event
      Button1.Click += new EventHandler(Button1_Click);
   }
   Button Button1 = new Button();
   void Button1_Click(object sender, Event e) {
      Console.WriteLine("Button1 was clicked!");
   }
   public void Disconnect() {
      Button1.Click -= new EventHandler(Button1_Click);
   }
}

shows a Form1 class that adds Button1_Click as an event handler for Button1’s Click event. In the Disconnect method, the event handler is removed.

The Button class could be rewritten to use a property-like event declaration rather than a field-like event declaration, as shown in the example

public class Button
{
      public event EventHandler Click {
         get {...}
         set {...}
      }
      public void Reset() {
         Click = null;
      }
}

This change has no effect on client code, but allows the Button class more implementation flexibility, as the event handler for Click does not necessarily have to be represented by a field.


1.7.6 Operators

An operator is a member that defines the meaning of an expression operator that can be applied to instances of the class. There are three kinds of operators that can be defined: unary operators, binary operators, and conversion operators.

The following examples defines a Digit type that represents decimal digits – integral values between 0 and 9.

using System;
public struct Digit
{
   byte value;
   public Digit(byte value) {
      if (value < 0 || value > 9) throw new ArgumentException();
      this.value = value;
   }
   public Digit(int value): this((byte) value) {}
   public static implicit operator byte(Digit d) {
      return d.value;
   }
   public static explicit operator Digit(byte b) {
      return new Digit(b);
   }
   public static Digit operator+(Digit a, Digit b) {
      return new Digit(a.value + b.value);
   }
   public static Digit operator-(Digit a, Digit b) {
      return new Digit(a.value - b.value);
   }
   public static bool operator==(Digit a, Digit b) {
      return a.value == b.value;
   }
   public static bool operator!=(Digit a, Digit b) {
      return a.value != b.value;
   }
   public override bool Equals(object value) {
      return this == (Digit) value;
   }
   public override int GetHashCode() {
      return value.GetHashCode();
   }
   public override string ToString() {
      return value.ToString();
   }
}
class Test
{
   static void Main() {
      Digit a = (Digit) 5;
      Digit b = (Digit) 3;
      Digit plus = a + b;
      Digit minus = a – b;
      bool equals = (a == b);
      Console.WriteLine("{0} + {1} = {2}", a, b, plus);
      Console.WriteLine("{0} - {1} = {2}", a, b, minus);
      Console.WriteLine("{0} == {1} = {2}", a, b, equals);
   }
}

The Digit type defines the following operators:

  • An implicit conversion operator from Digit to byte.
  • An explicit conversion operator from byte to Digit.
  • An addition operator that adds two Digit values and returns a Digit value.
  • A subtraction operator that subtracts one Digit value from another, and returns a Digit value.
  • The equality (==) and inequality (!=) operators, which compare two Digit values.


1.7.7 Indexers

An indexer is a member that enables an object to be indexed in the same way as an array. Whereas properties enable field-like access, indexers enable array-like access.

As an example, consider the Stack class presented earlier. This class might want to expose array-like access so that it is possible to inspect or alter the items on the stack without performing unnecessary Push and Pop operations. The Stack is implemented as a linked list, but wants to provide the convenience of array access.

Indexer declarations are similar to property declarations, with the main differences being that indexers are nameless (the “name” used in the declaration is this, since this is being indexed) and that indexers include indexing parameters. The indexing parameters are provided between square brackets. The example

using System;
public class Stack
{
   private Node GetNode(int index) {
      Node temp = first;
      while (index > 0) {
         temp = temp.Next;
         index--;
      }
      return temp;
   }
   public object this[int index] {
      get {
         if (!ValidIndex(index))
            throw new Exception("Index out of range.");
         else
            return GetNode(index).Value;
      }
      set {
         if (!ValidIndex(index))
            throw new Exception("Index out of range.");
         else
            GetNode(index).Value = value;
      }
   }
   ...
}
class Test
{
   static void Main() {
      Stack s = new Stack();
      s.Push(1);
      s.Push(2);
      s.Push(3);
      s[0] = 33;   // Changes the top item from 3 to 33
      s[1] = 22;   // Changes the middle item from 2 to 22
      s[2] = 11;   // Changes the bottom item from 1 to 11
   }
}

shows an indexer for the Stack class.


1.7.8 Instance constructors

An instance constructor is a member that implements the actions required to initialize an instance of a class.

The example

using System;
class Point
{
   public double x, y;
   public Point() {
      this.x = 0;
      this.y = 0;
   }
   public Point(double x, double y) {
      this.x = x;
      this.y = y;
   }
   public static double Distance(Point a, Point b) {
      double xdiff = a.x – b.x;
      double ydiff = a.y – b.y;
      return Math.Sqrt(xdiff * xdiff + ydiff * ydiff);
   }
   public override string ToString() {
      return string.Format("({0}, {1})", x, y);
   }
}
class Test
{
   static void Main() {
      Point a = new Point();
      Point b = new Point(3, 4);
      double d = Point.Distance(a, b);
      Console.WriteLine("Distance from {0} to {1} is {2}", a, b, d);
   }
}

shows a Point class that provides two public constructors. One Point constructor takes no arguments, and the other takes two double arguments.

If no constructor is supplied for a class, then an empty constructor with no parameters is automatically provided.


1.7.9 Destructors

A destructor is a member that implements the actions required to destruct an instance of a class. Destructors cannot take parameters, cannot have accessibility modifiers, and cannot be called explicitly. The destructor for an instance is called automatically during garbage collection.

The example

using System;
class Point
{
   public double x, y;
   public Point(double x, double y) {
      this.x = x;
      this.y = y;
   }
   ~Point() {
      Console.WriteLine("Destructed {0}", this);
   }
   public override string ToString() {
      return string.Format("({0}, {1})", x, y);
   }
}

shows a Point class with a destructor.


1.7.10 Static constructors

A static constructor is a member that implements the actions required to initialize a class. Static constructors cannot take parameters, cannot have accessibility modifiers, and cannot be called explicitly. The static constructor for a class is called automatically when the class is loaded.

The example

using System.Data;
class Employee
{
   private static DataSet ds;
   static Employee() {
      ds = new DataSet(...);
   }
   public string Name;
   public decimal Salary;
   ...
}

shows an Employee class with a static constructor that initializes a static field.


1.7.11 Inheritance

Classes support single inheritance, and the type object is the ultimate base class for all classes.

The classes shown in earlier examples all implicitly derive from object. The example

class A
{
   public void F() { Console.WriteLine("A.F"); }
}

shows a class A that implicitly derives from object. The example

class B: A
{
   public void G() { Console.WriteLine("B.G"); }
}
class Test
{
   static void Main() {
      B b = new B();
      b.F();         // Inherited from A
      b.G();         // Introduced in B

      A a = b;         // Treat a B as an A
      a.F();
   }
}

shows a class B that derives from A. The class B inherits A’s F method, and introduces a G method of its own.

Methods, properties, and indexers can be virtual, which means that their implementation can be overridden in derived classes. The example

using System;
class A
{
   public virtual void F() { Console.WriteLine("A.F"); }
}
class B: A
{
   public override void F() {
      base.F();
      Console.WriteLine("B.F");
   }
}
class Test
{
   static void Main() {
      B b = new B();
      b.F();
      A a = b;
      a.F();
   }
}

shows a class A with a virtual method F, and a class B that overrides F. The overriding method in B contains a call base.F() which calls the overridden method in A.

A class can indicate that it is incomplete, and is intended only as a base class for other classes by including the abstract modifier. Such a class is called an abstract class. An abstract class can specify abstract members – members that a non-abstract derived class must implement. The example

using System;
abstract class A
{
   public abstract F();
}
class B: A
{
   public override F() { Console.WriteLine("B.F"); }
}
class Test
{
   static void Main() {
      B b = new B();
      B.F();
      A a = b;
      a.F();
   }
}

introduces an abstract method F in the abstract class A. The non-abstract class B provides an implementation for this method.


1.8 Structs

The list of similarities between classes and structs is long – structs can implement interfaces, and can have the same kinds of members as classes. Structs differ from classes in several important ways, however: structs are value types rather than reference types, and inheritance is not supported for structs. Struct values are stored either “on the stack” or “in-line”. Careful programmers can sometimes enhance performance through judicious use of structs.

For example, the use of a struct rather than a class for a Point can make a large difference in the number of memory allocations performed by a program. The program below creates and initializes an array of 100 points. With Point implemented as a class, the program instantiates 101 separate objects – one for the array and one each for the 100 elements.

class Point
{
   public int x, y;
   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }
}
class Test
{
   static void Main() {
      Point[] points = new Point[100];
      for (int i = 0; i < 100; i++)
         points[i] = new Point(i, i*i);
   }
}

If Point is instead implemented as a struct, as in

struct Point
{
   public int x, y;
   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }
}

then the test program instantiates just one object – the one for the array. The Point instances are allocated in-line within the array. This optimization can be mis-used. Using structs instead of classes can also make a program slower and fatter, as passing a struct instance as a value parameter causes a copy of the struct to be created. There is no substitute for careful data structure and algorithm design.


1.9 Interfaces

An interface defines a contract. A class or struct that implements an interface must adhere to its contract. Interfaces can contain methods, properties, indexers, and events as members.

The example

interface IExample
{
   string this[int index] { get; set; }
   event EventHandler E;
   void F(int value);
   string P { get; set; }
}
public delegate void EventHandler(object sender, Event e);

shows an interface that contains an indexer, an event E, a method F, and a property P.

Interfaces may employ multiple inheritance. In the example

interface IControl
{
   void Paint();
}
interface ITextBox: IControl
{
   void SetText(string text);
}
interface IListBox: IControl
{
   void SetItems(string[] items);
}
interface IComboBox: ITextBox, IListBox {}

the interface IComboBox inherits from both ITextBox and IListBox.

Classes and structs can implement multiple interfaces. In the example

interface IDataBound
{
   void Bind(Binder b);
}
public class EditBox: Control, IControl, IDataBound
{
   public void Paint() {...}
   public void Bind(Binder b) {...}
}

the class EditBox derives from the class Control and implements both IControl and IDataBound.

In previous example, the Paint method from the IControl interface and the Bind method from IDataBound interface are implemented using public members on the EditBox class. C# provides an alternative way of implementing these methods that allows the implementing class to avoid having these members be public. Interface members can be implemented using a qualified name. For example, the EditBox class could instead be implemented by providing IControl.Paint and IDataBound.Bind methods.

public class EditBox: IControl, IDataBound
{
   void IControl.Paint() {...}
   void IDataBound.Bind(Binder b) {...}
}

Interface members implemented in this way are called explicit interface members because each member explicitly designates the interface member being implemented. Explicit interface members can only be called via the interface. For example, the EditBox’s implementation of the Paint method can be called only by casting to the IControl interface.

class Test
{
   static void Main() {
      EditBox editbox = new EditBox();
      editbox.Paint();   // error: no such method
      IControl control = editbox;
      control.Paint();   // calls EditBox’s Paint implementation
   }
}


1.10 Delegates

Delegates enable scenarios that C++ and some other languages have addressed with function pointers. Unlike function pointers, delegates are object-oriented, type-safe, and secure.

Delegates are reference types that derive from a common base class: System.Delegate. A delegate instance encapsulates a method – a callable entity. For instance methods, a callable entity consists of an instance and a method on the instance. For static methods, a callable entity consists of a class and a static method on the class.

An interesting and useful property of a delegate is that it does not know or care about the type of the object that it references. Any object will do; all that matters is that the method’s signature matches the delegate’s. This makes delegates perfectly suited for “anonymous” invocation. This is a powerful capability.

There are three steps in defining and using delegates: declaration, instantiation, and invocation. Delegates are declared using delegate declaration syntax. The example

delegate void SimpleDelegate();

declares a delegate named SimpleDelegate that takes no arguments and returns void.

The example

class Test
{
   static void F() {
      System.Console.WriteLine("Test.F");
   }
   static void Main() {
      SimpleDelegate d = new SimpleDelegate(F);
      d();
   }
}

creates a SimpleDelegate instance and then immediately calls it.

There is not much point in instantiating a delegate for a method and then immediately calling via the delegate, as it would be simpler to call the method directly. Delegates show their usefulness when their anonymity is used. The example

void MultiCall(SimpleDelegate d, int count) {
   for (int i = 0; i < count; i++)
      d();
   }
}

shows a MultiCall method that repeatedly calls a SimpleDelegate. The MultiCall method doesn’t know or care what type method that is the target method for the SimpleDelegate, what accessibility this method has, or whether the method is static or non-static. All that matters is that the signature of the target method is compatible with SimpleDelegate.


1.11 Enums

An enum type declaration defines a type name for a related group of symbolic constants. Enums are used for “multiple choice” scenarios, in which a runtime decision is made from a fixed number of choices that are known at compile-time.

The example

enum Color
{
   Red,
   Blue,
   Green
}
class Shape
{
   public void Fill(Color color) {
      switch(color) {
         case Color.Red:
            ...
            break;
         case Color.Blue:
            ...
            break;
         case Color.Green:
            ...
            break;
         default:
            break;
      }
   }
}

shows a Color enum and a method that uses this enum. The signature of the Fill method makes it clear that the shape can be filled with one of the given colors.

The use of enums is superior to the use of integer constants – as is common in languages without enums – because the use of enums makes the code more readable and self-documenting. The self-documenting nature of the code also makes it possible for the development tool to assist with code writing and other “designer” activities. For example, the use of Color rather than int for a parameter type enables smart code editors to suggest Color values.


1.12 Namespaces and assemblies

The programs presented so far have stood on their own except for dependence on a few system-provided classes such as the System.Console class. It is far more common for real-world programs to consist of several different pieces. For example, a corporate application might depend on several different components, including some developed internally and some purchased from independent software vendors.

Namespaces and assemblies enable this component-based system. Namespaces provide a logical organizational system. Namespaces are used both as an “internal” organization system for a program, and as an “external” organization system – a way of presenting program elements that are exposed to other programs.

Assemblies are used for physical packaging and deployment. An assembly acts as a container for types. An assembly may contain types, the executable code used to implement these types, and references to other assemblies.

There are two main kinds of assemblies: applications and libraries. Applications have a main entry point and usually have a file extension of .exe; libraries do not have a main entry point, and usually have a file extension of .dll.

To demonstrate the use of namespaces and assemblies, this section revisits the “Hello, world” program presented earlier, and splits it into two pieces: a library that provides messages and a console application that displays them.

The library will contain a single class named HelloMessage. The example

// HelloLibrary.cs
namespace Microsoft.CSharp.Introduction
{
   public class HelloMessage
   {
      public string Message {
         get {
            return "Hello, world";
         }
      }
   }
}

shows the HelloMessage class in a namespace named Microsoft.CSharp.Introduction. The HelloMessage class provides a read-only property named Message. Namespaces can nest, and the declaration

namespace Microsoft.CSharp.Introduction
{...}

is shorthand for several levels of namespace nesting:

namespace Microsoft
{
   namespace CSharp
   {
      namespace Introduction
      {....}
   }
}

The next step in the componentization of “Hello, world” is to write a console application that uses the HelloMessage class. The fully qualified name for the class – Microsoft.CSharp.Introduction.HelloMessage – could be used, but this name is quite long and unwieldy. An easier way is to use a using namespace directive, which makes it possible to use all of the types in a namespace without qualification. The example

// HelloApp.cs
using Microsoft.CSharp.Introduction;
class HelloApp
{
   static void Main() {
      HelloMessage m = new HelloMessage();
      System.Console.WriteLine(m.Message);
   }
}

shows a using namespace directive that refers to the Microsoft.CSharp.Introduction namespace. The occurrences of HelloMessage are shorthand for Microsoft.CSharp.Introduction.HelloMessage.

C# also enables the definition and use of aliases. A using alias directive defines an alias for a type. Such aliases can be useful in situation in which name collisions occur between two libraries, or when a small number of types from a much larger namespace are being used. The example

using MessageSource = Microsoft.CSharp.Introduction.HelloMessage;

shows a using alias directive that defines MessageSource as an alias for the HelloMessage class.

The code we have written can be compiled into a library containing the class HelloMessage and an application containing the class HelloApp. The details of this compilation step might differ based on the compiler or tool being used. Using the command-line compiler provided in Visual Studio 7.0, the correct invocations are

csc /target:library HelloLibrary.cs

which produces the class library HelloLibrary.dll and

csc /reference:HelloLibrary.dll HelloApp.cs

which produces the application HelloApp.exe.


1.13 Versioning

Versioning is the process of evolving a component over time in a compatible manner. A new version of a component is source compatible with a previous version if code that depends on the previous version can, when recompiled, work with the new version. In contrast, a new version of a component is binary compatible if a program that depended on the old version can, without recompilation, work with the new version.

Most languages do not support binary compatibility at all, and many do little to facilitate source compatibility. In fact, some languages contain flaws that make it impossible, in general, to evolve a class over time without breaking at least some client code.

As an example, consider the situation of a base class author who ships a class named Base. In the first version, Base contains no F method. A component named Derived derives from Base, and introduces an F. This Derived class, along with the class Base that it depends on, is released to customers, who deploy to numerous clients and servers.

// Author A
namespace A
{
   public class Base      // version 1
   {
   }
}
// Author B
namespace B
{
   class Derived: A.Base
   {
      public virtual void F() {
         System.Console.WriteLine("Derived.F");
      }
   }
}

So far, so good. But now the versioning trouble begins. The author of Base produces a new version, and adds its own F method.

// Author A
namespace A
{
   public class Base      // version 2
   {
      public virtual void F() {      // added in version 2
         System.Console.WriteLine("Base.F");
      }
   }
}

This new version of Base should be both source and binary compatible with the initial version. (If it weren’t possible to simply add a method then a base class could never evolve.) Unfortunately, the new F in Base makes the meaning of Derived’s F unclear. Did Derived mean to override Base’s F? This seems unlikely, since when Derived was compiled, Base did not even have an F! Further, if Derived’s F does override Base’s F, then it must adhere to the contract specified by Base – a contract that was unspecified when Derived was written? In some cases, this is impossible. For example, the contract of Base’s F might require that overrides of it always call the base. Derived’s F could not possibly adhere to such a contract.

C# addresses this versioning problem by requiring developers to clearly state their intent. In the original code example, the code was clear, since Base did not even have an F. Clearly, Derived’s F is intended as a new method rather than an override of a base method, since no base method named F exists.

// Author A
namespace A
{
   public class Base
   {
   }
}
// Author B
namespace B
{
   class Derived: A.Base
   {
      public virtual void F() {
         System.Console.WriteLine("Derived.F");
      }
   }
}

If Base adds an F and ships a new version, then the intent of a binary version of Derived is still clear – Derived’s F is semantically unrelated, and should not be treated as an override.

However, when Derived is recompiled, the meaning is unclear – the author of Derived may intend its F to override Base’s F, or to hide it. Since the intent is unclear, the compiler produces a warning, and by default makes Derived’s F hide Base’s F. This course of action duplicates the semantics for the case in which Derived is not recompiled. The warning that is generated alerts Derived’s author to the presence of the F method in Base.

If Derived’s F is semantically unrelated to Base’s F, then Derived’s author can express this intent – and, in effect, turn off the warning – by using the new keyword in the declaration of F.

// Author A
namespace A
{
   public class Base         // version 2
   {
      public virtual void F() { // added in version 2
         System.Console.WriteLine("Base.F");
      }
   }
}
// Author B
namespace B
{
   class Derived: A.Base   // version 2a: new
   {
      new public virtual void F() {
         System.Console.WriteLine("Derived.F");
      }
   }
}

On the other hand, Derived’s author might investigate further, and decide that Derived’s F should override Base’s F. This intent can be specified by using the override keyword, as shown below.

// Author A
namespace A
{
   public class Base               // version 2
   {
      public virtual void F() { // added in version 2
         System.Console.WriteLine("Base.F");
      }
   }
}
// Author B
namespace B
{
   class Derived: A.Base   // version 2b: override
   {
      public override void F() {
         base.F();
         System.Console.WriteLine("Derived.F");
      }
   }
}

The author of Derived has one other option, and that is to change the name of F, thus completely avoiding the name collision. Though this change would break source and binary compatibility for Derived, the importance of this compatibility varies depending on the scenario. If Derived is not exposed to other programs, then changing the name of F is likely a good idea, as it would improve the readability of the program – there would no longer be any confusion about the meaning of F.


1.14 Attributes

C# is a procedural language, but like all procedural languages it does have some declarative elements. For example, the accessibility of a method in a class is specified by decorating it public, protected, internal, protected internal, or private. Through its support for attributes, C# generalizes this capability, so that programmers can invent new kinds of declarative information, specify this declarative information for various program entities, and retrieve this declarative information at run-time. Programs specify this additional declarative information by defining and using attributes.

For instance, a framework might define a HelpAttribute attribute that can be placed on program elements such as classes and methods, enabling developers to provide a mapping from program elements to documentation for them. The example

using System;
[AttributeUsage(AttributeTargets.All)]
public class HelpAttribute: Attribute
{
   public HelpAttribute(string url) {
      this.url = url;
   }
   public string Topic = null;
   private string url;
   public string Url {
      get { return url; }
   }
}

defines an attribute class named HelpAttribute, or Help for short, that has one positional parameter (string url) and one named argument (string Topic). Positional parameters are defined by the formal parameters for public constructors of the attribute class, and named parameters are defined by public read-write properties of the attribute class.

The example

[Help("http://www.mycompany.com/…/Class1.htm")]
public class Class1
{
   [Help("http://www.mycompany.com/…/Class1.htm", Topic = "F")]
   public void F() {}
}

shows several uses of the attribute.

Attribute information for a given program element can be retrieved at run-time by using reflection support. The example

using System;
class Test
{
   static void Main() {
      Type type = typeof(Class1);
      object[] arr = type.GetCustomAttributes(typeof(HelpAttribute));
      if (arr.Length == 0)
         Console.WriteLine("Class1 has no Help attribute.");
      else {
         HelpAttribute ha = (HelpAttribute) arr[0];
         Console.WriteLine("Url = {0}, Topic = {1}", ha.Url, ha.Topic);
      }
   }
}

checks to see if Class1 has a Help attribute, and writes out the associated Topic and Url values if the attribute is present.


2. Lexical structure

© 1987-2000 Microsoft Corporation. All rights reserved.


2.1 Phases of translation

A C# program consists of one or more source files. A source file is an ordered sequence of Unicode characters. Source files typically have a one-to-one correspondence with files in a file system, but this correspondence is not required.

Conceptually speaking, a program is compiled using three steps:

  • Pre-processing, a text-to-text translation that enables conditional inclusion and exclusion of program text.
  • Lexical analysis, which translates a stream of input characters into a stream of tokens.
  • Syntactic analysis, which translates the stream of tokens into executable code.


2.2 Grammar notation

Lexical and syntactic grammars for C# are interspersed throughout this specification. The lexical grammar defines how characters can be combined to form tokens; the syntactic grammar defines how tokens can be combined to form C# programs.

Grammar productions include non-terminal symbols and terminal symbols. In grammar productions, non-terminal symbols are shown in italic type, and terminal symbols are shown in a fixed-width font. Each non-terminal is defined by a set of productions. The first line of a set of productions is the name of the non-terminal, followed by a colon. Each successive indented line contains the right-hand side for a production that has the non-terminal symbol as the left-hand side. The example:

nonsense:
terminal1
terminal2

defines the nonsense non-terminal as having two productions, one with terminal1 on the right-hand side and one with terminal2 on the right-hand side.

Alternatives are normally listed on separate lines, though in cases where there are many alternatives, the phrase “one of” precedes a list of the options. This is simply shorthand for listing each of the alternatives on a separate line. The example:

letter: one of
A B C a b c

is shorthand for:

letter: one of
A
B
C
a
b
c

A subscripted suffix “opt”, as in identifieropt, is used as shorthand to indicate an optional symbol. The example:

whole:
first-part second-partopt last-part

is shorthand for:

whole:
first-part last-part
first-part second-part last-part


2.3 Pre-processing

The pre-processing phase is a text-to-text translation phase that enables conditional inclusion and exclusion of code through pre-processing.

pp-unit:
pp-groupopt
pp-group:
pp-group-part
pp-group pp-group-part
pp-group-part:
pp-tokensopt new-line
pp-declaration
pp-if-section
pp-control-line
pp-line-number
pp-tokens:
pp-token
pp-tokens pp-token
pp-token:
identifier
keyword
literal
operator-or-punctuator
new-line:
The carriage return character (U+000D)
The line feed character (U+000A)
The carriage return character followed by a line feed character
The line separator character (U+2028)
The paragraph separator character (U+2029)


2.3.1 Pre-processing declarations

Names can be defined and undefined for use in pre-processing. A #define defines an identifier within the scope of a file. A #undef "undefines" an identifier within the scope of a file – if the identifier was defined earlier then it becomes undefined. If an identifier is defined then it is semantically equivalent to true; if an identifier is undefined then it is semantically equivalent to false.

pp-declaration:
#define pp-identifier
#undef pp-identifier

The example:

#define A
#undef B
class C
{
#if A
   void F() {}
#else
   void G() {}
#endif
#if B
   void H() {}
#else
   void I() {}
#endif
}

becomes:

class C
{
   void F() {}
   void I() {}
}

Within a pp-unit, declarations must precede pp-token elements. In other words, #define and #undef must precede any "real code" in the file, or a compile-time error occurs. Thus, it is possible to intersperse #if and #define as in the example below:

#define A
#if A
   #define B
#endif
namespace N
{
   #if B
   class Class1 {}
   #endif
}

The following example is illegal because a #define follows real code:

#define A
namespace N
{
   #define B
   #if B
   class Class1 {}
   #endif
}

A #undef may "undefine" a name that is not defined. The example below defines a name and then undefines it twice; the second #undef has no effect but is still legal.

#define A
#undef A
#undef A


2.3.2 #if, #elif, #else, #endif

A pp-if-section is used to conditionally include or exclude portions of program text.

pp-if-section:
pp-if-group pp-elif-groupsopt pp-else-groupopt pp-endif-line
pp-if-group:
#if pp-expression new-line pp-groupopt
pp-elif-groups
pp-elif-group
pp-elif-groups pp-elif-group
pp-elif-group:
#elif pp-expression new-line groupopt
pp-else-group:
#else new-line groupopt
pp-endif-line
#endif new-line

The example:

#define Debug
class Class1
{
#if Debug
   void Trace(string s) {}
#endif
}

becomes:

class Class1
{
   void Trace(string s) {}
}

If sections can nest. Example:

#define Debug      // Debugging on
#undef Trace      // Tracing off
class PurchaseTransaction
{
   void Commit() {
      #if Debug
         CheckConsistency();
         #if Trace
            WriteToLog(this.ToString());
         #endif
      #endif
      CommitHelper();
   }
}


2.3.3 Pre-processing control lines

The #error and #warning features enable code to report error and warning conditions to the compiler for integration with standard compile-time errors and warnings.

pp-control-line:
#error pp-message
#warning pp-message
pp-message:
pp-tokensopt

The example

#warning Code review needed before check-in
#define DEBUG
#if DEBUG && RETAIL
   #error A build can't be both debug and retail!
#endif
class Class1
{…}

always produces a warning ("Code review needed before check-in"), and produces an error if the pre-processing identifiers DEBUG and RETAIL are both defined.


2.3.4 #line

The #line feature enables a developer to alter the line number and source file names that are used by the compiler in output such as warnings and errors. If no line directives are present then the line number and file name are determined automatically by the compiler. The #line directive is most commonly used in meta-programming tools that generate C# source code from some other text input.

pp-line-number:
#line integer-literal
#line integer-literal string-literal
pp-integer-literal:
decimal-digit
decimal-digits decimal-digit
pp-string-literal:
" pp-string-literal-characters "
pp-string-literal-characters:
pp-string-literal-character
pp-string-literal-characters pp-string-literal-character
pp-string-literal-character:
Any character except " (U+0022), and white-space


2.3.5 Pre-processing identifiers

Pre-processing identifiers employ a grammar similar to the grammar used for regular C# identifiers:

pp-identifier:
pp-available-identifier
pp-available-identifier:
A pp-identifier-or-keyword that is not true or false
pp-identifier-or-keyword:
identifier-start-character identifier-part-charactersopt

The symbols true and false are not legal pre-processing identifiers, and so cannot be defined with #define or undefined with #undef.


2.3.6 Pre-processing expressions

The operators !, ==, !=, && and || are permitted in pre-processing expressions. Parentheses can be used for grouping in pre-processing expressions.

pp-expression:
pp-equality-expression
pp-primary-expression:
true
false
pp-identifier
( pp-expression )
pp-unary-expression:
pp-primary-expression
! pp-unary-expression
pp-equality-expression:
pp-equality-expression == pp-logical-and-expression
pp-equality-expression != pp-logical-and-expression
pp-logical-and-expression:
pp-unary-expression
pp-logical-and-expression && pp-unary-expression
pp-logical-or-expression:
pp-logical-and-expression
pp-logical-or-expression || pp-logical-and-expression


2.3.7 Interaction with whitespace

Conditional compilation directives must be the first non-whitespace for a line.

A single-line comment may follow on the same line as conditional-compilation directives other than pp-control-line directives. For example,

#define Debug         // Defined if the build is a debug build

For pp-control-line directives, the remainder of the line constitutes the pp-message, independent of the contents of the line. The example

#warning             // TODO: Add a better warning

results in a warning with the contents "// TODO: Add a better warning".

A multi-line comment may not begin or end on the same line as a conditional compilation directive. The example

/* This comment is illegal because it
ends on the same line*/ #define Debug
/* This is comment is illegal because it is on the same line */ #define Retail
#define A /* This is comment is illegal because it is on the same line */
#define B /* This comment is illegal because it starts
on the same line */

results in a compile-time error.

Text that otherwise might form a conditional compilation directive can be hidden in a comment. The example

// This entire line is a commment.   #define Debug
/* This text would be a cc directive but it is commented out:
      #define Retail
*/

contains no conditional compilation directives, and consists entirely of whitespace.


2.4 Lexical analysis

The lexical analysis phase translates a stream of input characters into a stream of tokens.


2.4.1 Input

input:
input-elementsopt
input-elements:
input-element
input-elements input-element
input-element:
comment
white-space
token


2.4.2 Input characters

input-character:
any Unicode character


2.4.3 Line terminators

line-terminator:
The carriage return character (U+000D)
The line feed character (U+000A)
The carriage return character followed by a line feed character
The line separator character (U+2028)
The paragraph separator character (U+2029)


2.4.4 Comments

Two forms of comments are supported: regular comments and one-line comments.

A regular comment begins with the characters /* and ends with the characters */. Regular comments can occupy a portion of a line, a single line, or multiple lines. The example

/* Hello, world program
      This program writes “Hello, world” to the console
*/
class Hello
{
   static void Main() {
      Console.WriteLine("Hello, world");
   }
}

includes a regular comment.

A one-line comment begins with the characters // and extends to the end of the line. The example

// Hello, world program
//      This program writes “Hello, world” to the console
//
class Hello // any name will do for this class
{
   static void Main() { // this method must be named "Main"
      Console.WriteLine("Hello, world");
   }
}

shows several one-line comments.

comment:
regular-comment
one-line-comment
regular-comment:
/ * rest-of-regular-comment
rest-of-regular-comment:
* rest-of-regular-comment-star
not-star rest-of-regular-comment
rest-of-regular-comment-star:
/
* rest-of-regular-comment-star
not-star-or-slash rest-of-regular-comment
not-star:
Any input-character except *
not-star-or-slash:
Any input-character except * and /
one-line-comment:
/ / one-line-comment-text line-terminator
one-line-comment-text:
input-character
one-line-comment-text input-character


2.4.5 White space

white-space:
new-line
The tab character (U+0009)
The vertical tab character (U+000B)
The form feed character (U+000C)
The "control-Z" or "substitute" character (U+001A)
All characters with Unicode class "Zs"


2.4.6 Tokens

There are several kinds of tokens: identifiers, keywords, literals, operators, and punctuators. White space is ignored, though it may act as a separator for tokens.token:
identifier
keyword
literal
operator-or-punctuator


2.5 Syntactic analysis

The syntactic analysis phase translates a stream of tokens into executable code.


2.5.1 Identifiers

These identifier rules exactly correspond to those recommended by the Unicode 3.0 standard, Technical Report 15, Annex 7, except that underscore and similar characters are allowed as initial characters, Unicode escape characters are permitted in identifiers, and the “@” character is allowed as a prefix to enable keywords to be used as identifiers.

identifier:
available-identifier
@ identifier-or-keyword
available-identifier:
An identifier-or-keyword that is not a keyword
identifier-or-keyword:
identifier-start-character identifier-part-charactersopt
identifier-start-character:
letter-character
connecting-character
identifier-part-characters:
identifier-part-character
identifier-part-characters identifier-part-character
identifier-part-character:
letter-character
decimal-digit-character
connecting-character
combining-character
formatting-character
letter-character:
A Unicode character of classes Lu, Ll, Lt, Lm, Lo, or Nl
A unicode-character-escape-sequence representing a character of classes Lu, Ll, Lt, Lm, Lo, or Nl
combining-character:
A Unicode character of classes Mn or Mc
A unicode-character-escape-sequence representing a character of classes Mn or Mc
decimal-digit-character:
A Unicode character of the class Nd
A unicode-character-escape-sequence representing a character of the class Nd
connecting-character:
A Unicode character of the class Pc
A unicode-character-escape-sequence representing a character of the class Pc
formatting-character:
A Unicode character of the class Cf
A unicode-character-escape-sequence representing a character of the class Cf

Examples of legal identifiers include “identifier1”, “_identifier2”, and “@if”.

The prefix “@” enables the use of keywords as identifiers, which is useful when interfacing with other programming languages. The character @ is not actually part of the identifier, and so might be seen in other languages as a normal identifier, without the prefix. Use of the @ prefix for identifiers that are not keywords is permitted, but strongly discouraged as a matter of style.

The example:

class @class
{
   static void @static(bool @bool) {
      if (@bool)
         Console.WriteLine("true");
      else
         Console.WriteLine("false");
   }
}
class Class1
{
   static void M {
      @class.@static(true);
   }
}

defines a class named “class” with a static method named “static” that takes a parameter named “bool”.

Two identifiers are considered the same if they are identical after the following transformations are applied, in order:

  • The prefix “@”, if used, is removed.
  • Each unicode-character-escape-sequence is transformed into its corresponding Unicode character
  • The identifier is normalized via Normalization Form C (see Unicode Consortium Technical Report 15).


2.5.2 Keywords

A keyword is an identifier-like sequence of characters that is reserved, and cannot be used as an identifier except when prefaced by the @ character.

keyword: one of
abstract as base bool break
byte case catch char checked
class const continue decimal default
delegate do double else enum
event explicit extern false finally
fixed float for foreach goto
if implicit in int interface
internal is lock long namespace
new null object operator out
override params private protected public
readonly ref return sbyte sealed
short sizeof stackalloc static string
struct switch this throw true
try typeof uint ulong unchecked
unsafe ushort using virtual void
while


2.5.3 Literals

A literal is a source code representation of a value.

literal:
boolean-literal
integer-literal
real-literal
character-literal
string-literal
null-literal


2.5.3.1 Boolean literals

There are two boolean literal values: true and false.

boolean-literal:
true
falseThe type of a boolean-literal is bool.


2.5.3.2 Integer literals

Integer literals have two possible forms: decimal and hexadecimal.

integer-literal:
decimal-integer-literal
hexadecimal-integer-literal
decimal-integer-literal:
decimal-digits integer-type-suffixopt
decimal-digits:
decimal-digit
decimal-digits decimal-digit
decimal-digit: one of
0 1 2 3 4 5 6 7 8 9
integer-type-suffix: one of
U u L l UL Ul uL ul LU Lu lU lu
hexadecimal-integer-literal:
0x hex-digits integer-type-suffixopt
hex-digits:
hex-digit
hex-digits hex-digit
hex-digit: one of
0 1 2 3 4 5 6 7 8 9 A B C D E F a b c d e f

The type of an integer literal is determined as follows:

  • If the literal has no suffix, it has the first of these types in which its value can be represented: int, uint, long, ulong.
  • If the literal is suffixed by U or u, it has the first of these types in which its value can be represented: uint, ulong.
  • If the literal is suffixed by L or l, it has the first of these types in which its value can be represented: long, ulong.
  • If the literal is suffixed by UL, Ul, uL, ul, LU, Lu, lU, or lu, it is of type ulong.

If the value represented by an integer literal is outside the range of the ulong type, an error occurs.

To permit the smallest possible int and long values to be written as decimal integer literals, the following two rules exist:

  • When a decimal-integer-literal with the value 2147483648 (231) and no integer-type-suffix appears as the operand of the unary minus operator (§7.6.2), the result is a constant of type int with the value -2147483648 (-231). In all other situations, such a decimal-integer-literal is of type uint.
  • When a decimal-integer-literal with the value 9223372036854775808 (263) and no integer-type-suffix or the integer-type-suffix L or l appears as the operand of the unary minus operator (§7.6.2), the result is a constant of type long with the value -9223372036854775808 (-263). In all other situations, such a decimal-integer-literal is of type ulong.


2.5.3.3 Real literals

real-literal:
decimal-digits . decimal-digits exponent-partopt real-type-suffixopt
. decimal-digits exponent-partopt real-type-suffixopt
decimal-digits exponent-part real-type-suffixopt
decimal-digits real-type-suffix
exponent-part:
e signopt decimal-digits
E signopt decimal-digits
sign: one of
+ -
real-type-suffix: one of
F f D d M m

If no real type suffix is specified, the type of the real literal is double. Otherwise, the real type suffix determines the type of the real literal, as follows:

  • A real literal suffixed by F or f is of type float. For example, the literals 1f, 1.5f, 1e10f, and 123.456F are all of type float.
  • A real literal suffixed by D or d is of type double. For example, the literals 1d, 1.5d, 1e10d, and 123.456D are all of type double.
  • A real literal suffixed by M or m is of type decimal. For example, the literals 1m, 1.5m, 1e10m, and 123.456M are all of type decimal.

If the specified literal cannot be represented in the indicated type, then a compile-time error occurs.


2.5.3.4 Character literals

A character literal represents a single character, and usually consists of a character in quotes, as in 'a'.

character-literal:
' character '
character:
single-character
simple-escape-sequence
hexadecimal-escape-sequence
unicode-character-escape-sequence
single-character:
Any character except ' (U+0027), \ (U+005C), and new-line
simple-escape-sequence: one of
\' \" \\ \0 \a \b \f \n \r \t \v
hexadecimal-escape-sequence:
\x hex-digit hex-digitopt hex-digitopt hex-digitopt

A character that follows a backslash character (\) in a simple-escape-sequence or hexadecimal-escape-sequence must be one of the following characters: ', ", \, 0, a, b, f, n, r, t, x, v. Otherwise, a compile-time error occurs.

A simple escape sequence represents a Unicode character encoding, as described in the table below.

Escape sequence | Character name | Unicode encoding
\' | Single quote | 0x0027
\" | Double quote | 0x0022
\\ | Backslash | 0x005C
\0 | Null | 0x0000
\a | Alert | 0x0007
\b | Backspace | 0x0008
\f | Form feed | 0x000C
\n | New line | 0x000A
\r | Carriage return | 0x000D
\t | Horizontal tab | 0x0009
\v | Vertical tab | 0x000B

The type of a character-literal is char.


2.5.3.5 String literals

C# supports two forms of string literals: regular string literals and verbatim string literals. A regular string literal consists of zero or more characters enclosed in double quotes, as in "Hello, world", and may include both simple escape sequences (such as \t for the tab character) and hexadecimal escape sequences.

A verbatim string literal consists of an @ character followed by a double-quote character, zero or more characters, and a closing double-quote character. A simple example is @"Hello, world". In a verbatim string literal, the characters between the delimiters are interpreted verbatim, with the only exception being a quote-escape-sequence. In particular, simple escape sequences and hexadecimal escape sequences are not processed in verbatim string literals. A verbatim string literal may span multiple lines.

string-literal:
regular-string-literal
verbatim-string-literal
regular-string-literal:
" regular-string-literal-charactersopt "
regular-string-literal-characters:
regular-string-literal-character
regular-string-literal-characters regular-string-literal-character
regular-string-literal-character:
single-regular-string-literal-character
simple-escape-sequence
hexadecimal-escape-sequence
unicode-character-escape-sequence
single-regular-string-literal-character:
Any character except " (U+0022), \ (U+005C), and new-line
verbatim-string-literal:
@" verbatim -string-literal-charactersopt "
verbatim-string-literal-characters:
verbatim-string-literal-character
verbatim-string-literal-characters verbatim-string-literal-character
verbatim-string-literal-character:
single-verbatim-string-literal-character
quote-escape-sequence
single-verbatim-string-literal-character:
any character except "
quote-escape-sequence:
""

The example

string a = "hello, world";                  // hello, world
string b = @"hello, world";               // hello, world
string c = "hello \t world";               // hello     world
string d = @"hello \t world";               // hello \t world
string e = "Joe said \"Hello\" to me";      // Joe said "Hello"
string f = @"Joe said ""Hello"" to me";   // Joe said "Hello"
string g = "\\\\sever\\share\\file.txt";   // \\server\share\file.txt
string h = @"\\server\share\file.txt";      // \\server\share\file.txt
string i = "one\ntwo\nthree";
string j = @"one
two
three";

shows a variety of string literals. The last string literal, j, is a verbatim string literal that spans multiple lines. The characters between the quotation marks, including white space such as newline characters, are preserved verbatim.

The type of a string-literal is string.


2.5.3.6 The null literal

null-literal:
null

The type of a null-literal is the null type.


2.5.4 Operators and punctuators

There are several kinds of operators and punctuators. Operators are used in expressions to describe operations involving one or more operands. For example, the expression a + b uses the + operator to add the two operands a and b. Punctuators are for grouping and separating. For example, the punctuator ; is used to separate statements that appear in statement lists.

operator-or-punctuator: one of
{ } [ ] ( ) . , : ;
+ - * / % & | ^ ! ~
= < > ? ++ -- && || << >>
== != <= >= += -= *= /= %= &=
|= ^= <<= >>= ->


2.5.5 Unicode character escape sequences

A Unicode character escape sequence represents a Unicode character. Unicode character escape sequences are permitted in identifiers, string literals, and character literals.

unicode-character-escape-sequence:
\u hex-digit hex-digit hex-digit hex-digit

Multiple translations are not performed. For instance, the string literal “\u005Cu005C” is equivalent to “\u005C” rather than “\\”. (The Unicode value \u005C is the character “\”.)

The example

class Class1
{
   static void Test(bool \u0066) {
      char c = '\u0066';
      if (\u0066)
         Console.WriteLine(c.ToString());
   }
}

shows several uses of \u0066, which is the character escape sequence for the letter “f”. The program is equivalent to

class Class1
{
   static void Test(bool f) {
      char c = 'f';
      if (f)
         Console.WriteLine(c.ToString());
   }
}


3. Basic concepts

© 1987-2000 Microsoft Corporation. All rights reserved.


3.1 Declarations

Declarations in a C# program define the constituent elements of the program. C# programs are organized using namespaces (§9), which can contain type declarations and nested namespace declarations. Type declarations (§9.5) are used to define classes (§10), structs (§11), interfaces (§13), enums (§14), and delegates (§15). The kinds of members permitted in a type declaration depends on the form of the type declaration. For instance, class declarations can contain declarations for instance constructors (§10.10), destructors (§10.11), static constructors (§10.12), constants (§10.3), fields (§10.4), methods (§10.5), properties (§10.6), events (§10.7), indexers (§10.8), operators (§10.9), and nested types.

A declaration defines a name in the declaration space to which the declaration belongs. Except for overloaded constructor, method, indexer, and operator names, it is an error to have two or more declarations that introduce members with the same name in a declaration space. It is never possible for a declaration space to contain different kinds of members with the same name. For example, a declaration space can never contain a field and a method by the same name.

There are several different types of declaration spaces, as described in the following.

  • Within all source files of a program, namespace-member-declarations with no enclosing namespace-declaration are members of a single combined declaration space called the global declaration space.
  • Within all source files of a program, namespace-member-declarations within namespace-declarations that have the same fully qualified namespace name are members of a single combined declaration space.
  • Each class, struct, or interface declaration creates a new declaration space. Names are introduced into this declaration space through class-member-declarations, struct-member-declarations, or interface-member-declarations. Except for overloaded constructor declarations and static constructor declarations, a class or struct member declaration cannot introduce a member by the same name as the class or struct. A class, struct, or interface permits the declaration of overloaded methods and indexers. A class or struct furthermore permits the declaration of overloaded constructors and operators. For instance, a class, struct, or interface may contain multiple method declarations with the same name, provided these method declarations differ in their signature (§3.4). Note that base classes do not contribute to the declaration space of a class, and base interfaces do not contribute to the declaration space of an interface. Thus, a derived class or interface is allowed to declare a member with the same name as an inherited member. Such a member is said to hide the inherited member.
  • Each enumeration declaration creates a new declaration space. Names are introduced into this declaration space through enum-member-declarations.
  • Each block or switch-block creates a separate declaration space for local variables. Names are introduced into this declaration space through local-variable-declarations. If a block is the body of a constructor or method declaration, the parameters declared in the formal-parameter-list are members of the block’s local variable declaration space. The local variable declaration space of a block includes any nested blocks. Thus, within a nested block it is not possible to declare a local variable with the same name as a local variable in an enclosing block.
  • Each block or switch-block creates a separate declaration space for labels. Names are introduced into this declaration space through labeled-statements, and the names are referenced through goto-statements. The label declaration space of a block includes any nested blocks. Thus, within a nested block it is not possible to declare a label with the same name as a label in an enclosing block.

The textual order in which names are declared is generally of no significance. In particular, textual order is not significant for the declaration and use of namespaces, types, constants, methods, properties, events, indexers, operators, constructors, destructors, and static constructors. Declaration order is significant in the following ways:

  • Declaration order for field declarations and local variable declarations determines the order in which their initializers (if any) are executed.
  • Local variables must be defined before they are used (§3.5).
  • Declaration order for enum member declarations (§14.2) is significant when constant-expression values are omitted.

The declaration space of a namespace is “open ended”, and two namespace declarations with the same fully qualified name contribute to the same declaration space. For example

namespace Megacorp.Data
{
   class Customer
   {
      ...
   }
}
namespace Megacorp.Data
{
   class Order
   {
      ...
   }
}

The two namespace declarations above contribute to the same declaration space, in this case declaring two classes with the fully qualified names Megacorp.Data.Customer and Megacorp.Data.Order. Because the two declarations contribute to the same declaration space, it would have been an error if each contained a declaration of a class with the same name.

The declaration space of a block includes any nested blocks. Thus, in the following example, the F and G methods are in error because the name i is declared in the outer block and cannot be redeclared in the inner block. However, the H and I methods are valid since the two i’s are declared in separate non-nested blocks.

class A
{
   void F() {
      int i = 0;
      if (true) {
         int i = 1;
      }
   }
void G() {
      if (true) {
         int i = 0;
      }
      int i = 1;
   }
void H() {
      if (true) {
         int i = 0;
      }
      if (true) {
         int i = 1;
      }
   }
void I() {
      for (int i = 0; i < 10; i++)
         H();
      for (int i = 0; i < 10; i++)
         H();
   }
}


3.2 Members

Namespaces and types have members. The members of an entity are generally available through the use of a qualified name that starts with a reference to the entity, followed by a “.” token, followed by the name of the member.

Members of a type are either declared in the type or inherited from the base class of the type. When a type inherits from a base class, all members of the base class, except constructors and destructors, become members of the derived type. The declared accessibility of a base class member does not control whether the member is inherited—inheritance extends to any member that isn’t a constructor or destructor. However, an inherited member may not be accessible in a derived type, either because of its declared accessibility (§3.3.1) or because it is hidden by a declaration in the type itself (§3.5.1.2).


3.2.1 Namespace members

Namespaces and types that have no enclosing namespace are members of the global namespace. This corresponds directly to the names declared in the global declaration space.

Namespaces and types declared within a namespace are members of that namespace. This corresponds directly to the names declared in the declaration space of the namespace.

Namespaces have no access restrictions. It is not possible to declare private, protected, or internal namespaces, and namespace names are always publicly accessible.


3.2.2 Struct members

The members of a struct are the members declared in the struct and the members inherited from class object.

The members of a simple type correspond directly to the members of the struct type aliased by the simple type:

  • The members of sbyte are the members of the System.SByte struct.
  • The members of byte are the members of the System.Byte struct.
  • The members of short are the members of the System.Int16 struct.
  • The members of ushort are the members of the System.UInt16 struct.
  • The members of int are the members of the System.Int32 struct.
  • The members of uint are the members of the System.UInt32 struct.
  • The members of long are the members of the System.Int64 struct.
  • The members of ulong are the members of the System.UInt64 struct.
  • The members of char are the members of the System.Char struct.
  • The members of float are the members of the System.Single struct.
  • The members of double are the members of the System.Double struct.
  • The members of decimal are the members of the System.Decimal struct.
  • The members of bool are the members of the System.Boolean struct.


3.2.3 Enumeration members

The members of an enumeration are the constants declared in the enumeration and the members inherited from class object.


3.2.4 Class members

The members of a class are the members declared in the class and the members inherited from the base class (except for class object which has no base class). The members inherited from the base class include the constants, fields, methods, properties, events, indexers, operators, and types of the base class, but not the constructors, destructors, and static constructors of the base class. Base class members are inherited without regard to their accessibility.

A class declaration may contain declarations of constants, fields, methods, properties, events, indexers, operators, constructors, destructors, static constructors, and types.

The members of object and string correspond directly to the members of the class types they alias:

  • The members of object are the members of the System.Object class.
  • The members of string are the members of the System.String class.


3.2.5 Interface members

The members of an interface are the members declared in the interface and in all base interfaces of the interface, and the members inherited from class object.


3.2.6 Array members

The members of an array are the members inherited from class System.Array.


3.2.7 Delegate members

The members of a delegate are the members inherited from class System.Delegate.


3.3 Member access

Declarations of members allow control over member access. The accessibility of a member is established by the declared accessibility (§3.3.1) of the member combined with the accessibility of the immediately containing type, if any.

When access to a particular member is allowed, the member is said to be accessible. Conversely, when access to a particular member is disallowed, the member is said to be inaccessible. Access to a member is permitted when the textual location in which the access takes place is included in the accessibility domain (§3.3.2) of the member.


3.3.1 Declared accessibility

The declared accessibility of a member can be one of the following:

  • Public, which is selected by including a public modifier in the member declaration. The intuitive meaning of public is “access not limited”.
  • Protected internal (meaning protected or internal), which is selected by including both a protected and an internal modifier in the member declaration. The intuitive meaning of protected internal is “access limited to this program or types derived from the containing class”.
  • Protected, which is selected by including a protected modifier in the member declaration. The intuitive meaning of protected is “access limited to the containing class or types derived from the containing class”.
  • Internal, which is selected by including an internal modifier in the member declaration. The intuitive meaning of internal is “access limited to this program”.
  • Private, which is selected by including a private modifier in the member declaration. The intuitive meaning of private is “access limited to the containing type”.

Depending on the context in which a member declaration takes place, only certain types of declared accessibility are permitted. Furthermore, when a member declaration does not include any access modifiers, the context in which the declaration takes place determines the default declared accessibility.

  • Namespaces implicitly have public declared accessibility. No access modifiers are allowed on namespace declarations.
  • Types declared in compilation units or namespaces can have public or internal declared accessibility and default to internal declared accessibility.
  • Class members can have any of the five types of declared accessibility and default to private declared accessibility. (Note that a type declared as a member of a class can have any of the five types of declared accessibility, whereas a type declared as a member of a namespace can have only public or internal declared accessibility.)
  • Struct members can have public, internal, or private declared accessibility and default to private declared accessibility. Struct members cannot have protected or protected internal declared accessibility.
  • Interface members implicitly have public declared accessibility. No access modifiers are allowed on interface member declarations.
  • Enumeration members implicitly have public declared accessibility. No access modifiers are allowed on enumeration member declarations.


3.3.2 Accessibility domains

The accessibility domain of a member is the (possibly disjoint) sections of program text in which access to the member is permitted. For purposes of defining the accessibility domain of a member, a member is said to be top-level if it is not declared within a type, and a member is said to be nested if it is declared within another type. Furthermore, the program text of a program is defined as all program text contained in all source files of the program, and the program text of a type is defined as all program text contained between the opening and closing “{” and “}” tokens in the class-body, struct-body, interface-body, or enum-body of the type (including, possibly, types that are nested within the type).

The accessibility domain of a predefined type (such as object, int, or double) is unlimited.

The accessibility domain of a top-level type T declared in a program P is defined as follows:

  • If the declared accessibility of T is public, the accessibility domain of T is the program text of P and any program that references P.
  • If the declared accessibility of T is internal, the accessibility domain of T is the program text of P.

From these definitions it follows that the accessibility domain of a top-level type is always at least the program text of the program in which the type is declared.

The accessibility domain of a nested member M declared in a type T within a program P is defined as follows (noting that M may itself possibly be a type):

  • If the declared accessibility of M is public, the accessibility domain of M is the accessibility domain of T.
  • If the declared accessibility of M is protected internal, the accessibility domain of M is the intersection of the accessibility domain of T with the program text of P and the program text of any type derived from T declared outside P.
  • If the declared accessibility of M is protected, the accessibility domain of M is the intersection of the accessibility domain of T with the program text of T and any type derived from T.
  • If the declared accessibility of M is internal, the accessibility domain of M is the intersection of the accessibility domain of T with the program text of P.
  • If the declared accessibility of M is private, the accessibility domain of M is the program text of T.

From these definitions it follows that the accessibility domain of a nested member is always at least the program text of the type in which the member is declared. Furthermore, it follows that the accessibility domain of a member is never more inclusive than the accessibility domain of the type in which the member is declared.

In intuitive terms, when a type or member M is accessed, the following steps are evaluated to ensure that the access is permitted:

  • First, if M is declared within a type (as opposed to a compilation unit or a namespace), an error occurs if that type is not accessible.
  • Then, if M is public, the access is permitted.
  • Otherwise, if M is protected internal, the access is permitted if it occurs within the program in which M is declared, or if it occurs within a class derived from the class in which M is declared and takes place through the derived class type (§3.3.3).
  • Otherwise, if M is protected, the access is permitted if it occurs within the class in which M is declared, or if it occurs within a class derived from the class in which M is declared and takes place through the derived class type (§3.3.3).
  • Otherwise, if M is internal, the access is permitted if it occurs within the program in which M is declared.
  • Otherwise, if M is private, the access is permitted if it occurs within the type in which M is declared.
  • Otherwise, the type or member is inaccessible, and an error occurs.

In the example

public class A
{
   public static int X;
   internal static int Y;
   private static int Z;
}
internal class B
{
   public static int X;
   internal static int Y;
   private static int Z;
   public class C
   {
      public static int X;
      internal static int Y;
      private static int Z;
   }
   private class D
   {
      public static int X;
      internal static int Y;
      private static int Z;
   }
}

the classes and members have the following accessibility domains:

  • The accessibility domain of A and A.X is unlimited.
  • The accessibility domain of A.Y, B, B.X, B.Y, B.C, B.C.X, and B.C.Y is the program text of the containing program.
  • The accessibility domain of A.Z is the program text of A.
  • The accessibility domain of B.Z and B.D is the program text of B, including the program text of B.C and B.D.
  • The accessibility domain of B.C.Z is the program text of B.C.
  • The accessibility domain of B.D.X, B.D.Y, and B.D.Z is the program text of B.D.

As the example illustrates, the accessibility domain of a member is never larger than that of a containing type. For example, even though all X members have public declared accessibility, all but A.X have accessibility domains that are constrained by a containing type.

As described in §3.2, all members of a base class, except for constructors and destructors, are inherited by derived types. This includes even private members of a base class. However, the accessibility domain of a private member includes only the program text of the type in which the member is declared. In the example

class A
{
   int x;
   static void F(B b) {
      b.x = 1;      // Ok
   }
}
class B: A
{
   static void F(B b) {
      b.x = 1;      // Error, x not accessible
   }
}

the B class inherits the private member x from the A class. Because the member is private, it is only accessible within the class-body of A. Thus, the access to b.x succeeds in the A.F method, but fails in the B.F method.


3.3.3 Protected access

When a protected member is accessed outside the program text of the class in which it is declared, and when a protected internal member is accessed outside the program text of the program in which it is declared, the access is required to take place through the derived class type in which the access occurs. Let B be a base class that declares a protected member M, and let D be a class that derives from B. Within the class-body of D, access to M can take one of the following forms:

  • An unqualified type-name or primary-expression of the form M.
  • A type-name of the form T.M, provided T is D or a class derived from D.
  • A primary-expression of the form E.M, provided the type of E is D or a class derived from D.
  • A primary-expression of the form base.M.

In addition to these forms of access, a derived class can access a protected constructor of a base class in a constructor-initializer (§10.10.1).

In the example

public class A
{
   protected int x;
   static void F(A a, B b) {
      a.x = 1;      // Ok
      b.x = 1;      // Ok
   }
}
public class B: A
{
   static void F(A a, B b) {
      a.x = 1;      // Error, must access through instance of B
      b.x = 1;      // Ok
   }
}

within A, it is possible to access x through instances of both A and B, since in either case the access takes place through an instance of A or a class derived from A. However, within B, it is not possible to access x through an instance of A, since A does not derive from B.


3.3.4 Accessibility constraints

Several constructs in the C# language require a type to be at least as accessible as a member or another type. A type T is said to be at least as accessible as a member or type M if the accessibility domain of T is a superset of the accessibility domain of M. In other words, T is at least as accessible as M if T is accessible in all contexts where M is accessible.

The following accessibility constraints exist:

  • The direct base class of a class type must be at least as accessible as the class type itself.
  • The explicit base interfaces of an interface type must be at least as accessible as the interface type itself.
  • The return type and parameter types of a delegate type must be at least as accessible as the delegate type itself.
  • The type of a constant must be at least as accessible as the constant itself.
  • The type of a field must be at least as accessible as the field itself.
  • The return type and parameter types of a method must be at least as accessible as the method itself.
  • The type of a property must be at least as accessible as the property itself.
  • The type of an event must be at least as accessible as the event itself.
  • The type and parameter types of an indexer must be at least as accessible as the indexer itself.
  • The return type and parameter types of an operator must be at least as accessible as the operator itself.
  • The parameter types of a constructor must be at least as accessible as the constructor itself.

In the example

class A {...}
public class B: A {...}

the B class is in error because A is not at least as accessible as B.

Likewise, in the example

class A {...}
public class B
{
   A F() {...}
   internal A G() {...}
   public A H() {...}
}

the H method in B is in error because the return type A is not at least as accessible as the method.


3.4 Signatures and overloading

Methods, constructors, indexers, and operators are characterized by their signatures:

  • The signature of a method consists of the name of the method and the type and kind (value, reference, or output) of each of its formal parameters. The signature of a method specifically does not include the return type, nor does it include the params modifier that may be specified for the last parameter.
  • The signature of a constructor consists of the type and kind (value, reference, or output) of each of its formal parameters. The signature of a constructor specifically does not include the params modifier that may be specified for the last parameter.
  • The signature of an indexer consists of the type of each of its formal parameters. The signature of an indexer specifically does not include the element type.
  • The signature of an operator consists of the name of the operator and the type of each of its formal parameters. The signature of an operator specifically does not include the result type.

Signatures are the enabling mechanism for overloading of members in classes, structs, and interfaces:

  • Overloading of methods permits a class, struct, or interface to declare multiple methods with the same name, provided the signatures of the methods are all unique.
  • Overloading of constructors permits a class or struct to declare multiple constructors, provided the signatures of the constructors are all unique.
  • Overloading of indexers permits a class, struct, or interface to declare multiple indexers, provided the signatures of the indexers are all unique.
  • Overloading of operators permits a class or struct to declare multiple operators with the same name, provided the signatures of the operators are all unique.

The following example shows a set of overloaded method declarations along with their signatures.

interface ITest
{
   void F();                     // F()
   void F(int x);                  // F(int)
   void F(ref int x);            // F(ref int)
   void F(out int x);            // F(out int)
   void F(int x, int y);         // F(int, int)
   int F(string s);               // F(string)
   int F(int x);                  // F(int)
   void F(string[] a);            // F(string[])
   void F(params string[] a);      // F(string[])
}

Note that the ref and out parameter modifiers (§10.5.1) are part of a signature. Thus, F(int), F(ref int), and F(out int) are all unique signatures. Also note that the return type and the params modifier are not part of a signature, and that it is not possible to overload solely based on return type or solely based on the inclusion or exclusion of the params modifier. Because of these restrictions, compiling the above example would produce errors for the methods with the duplicate signatures F(int) and F(string[]).


3.5 Scopes

The scope of a name is the region of program text within which it is possible to refer to the entity declared by the name without qualification of the name. Scopes can be nested, and an inner scope may redeclare the meaning of a name from an outer scope. The name from the outer scope is then said to be hidden in the region of program text covered by the inner scope, and access to the outer name is only possible by qualifying the name.

  • The scope of a namespace member declared by a namespace-member-declaration with no enclosing namespace-declaration is the entire program text of each compilation unit.
  • The scope of a namespace member declared by a namespace-member-declaration within a namespace-declaration whose fully qualified name is N is the namespace-body of every namespace-declaration whose fully qualified name is N or starts with the same sequence of identifiers as N.
  • The scope of a name defined or imported by a using-directive extends over the namespace-member-declarations of the compilation-unit or namespace-body in which the using-directive occurs. A using-directive may make zero or more namespace or type names available within a particular compilation-unit or namespace-body, but does not contribute any new members to the underlying declaration space. In other words, a using-directive is not transitive but rather affects only the compilation-unit or namespace-body in which it occurs.
  • The scope of a member declared by a class-member-declaration is the class-body in which the declaration occurs. In addition, the scope of a class member extends to the class-body of those derived classes that are included in the accessibility domain (§3.3.2) of the member.
  • The scope of a member declared by a struct-member-declaration is the struct-body in which the declaration occurs.
  • The scope of a member declared by an enum-member-declaration is the enum-body in which the declaration occurs.
  • The scope of a parameter declared in a constructor-declaration is the constructor-initializer and block of that constructor-declaration.
  • The scope of a parameter declared in a method-declaration is the method-body of that method-declaration.
  • The scope of a parameter declared in an indexer-declaration is the accessor-declarations of that indexer-declaration.
  • The scope of a parameter declared in an operator-declaration is the block of that operator-declaration.
  • The scope of a local variable declared in a local-variable-declaration is the block in which the declaration occurs. It is an error to refer to a local variable in a textual position that precedes the variable-declarator of the local variable.
  • The scope of a local variable declared in a for-initializer of a for statement is the for-initializer, the for-condition, the for-iterator, and the contained statement of the for statement.
  • The scope of a label declared in a labeled-statement is the block in which the declaration occurs.

Within the scope of a namespace, class, struct, or enumeration member it is possible to refer to the member in a textual position that precedes the declaration of the member. For example

class A
{
   void F() {
      i = 1;
   }
   int i = 0;
}

Here, it is valid for F to refer to i before it is declared.

Within the scope of a local variable, it is an error to refer to the local variable in a textual position that precedes the variable-declarator of the local variable. For example

class A
{
   int i = 0;
   void F() {
      i = 1;               // Error, use precedes declaration
      int i;
      i = 2;
   }
   void G() {
      int j = (j = 1);      // Legal
   }
   void H() {
      int a = 1, b = ++a;   // Legal
   }
}

In the F method above, the first assignment to i specifically does not refer to the field declared in the outer scope. Rather, it refers to the local variable and it is in error because it textually precedes the declaration of the variable. In the G method, the use of j in the initializer for the declaration of j is legal because the use does not precede the variable-declarator. In the H method, a subsequent variable-declarator legally refers to a local variable declared in an earlier variable-declarator within the same local-variable-declaration.

The scoping rules for local variables are designed to guarantee that the meaning of a name used in an expression context is always the same within a block. If the scope of a local variable was to extend only from its declaration to the end of the block, then in the example above, the first assignment would assign to the instance variable and the second assignment would assign to the local variable, possibly leading to errors if the statements of the block were later to be rearranged.

The meaning of a name within a block may differ based on the context in which the name is used. In the example

class Test
{
   static void Main() {
      string A = "hello, world";
      string s = A;                        // expression context
      Type t = typeof(A);                  // type context
      Console.WriteLine(s);               // writes "hello, world"
      Console.WriteLine(t.ToString());      // writes "Type: A"
   }
}

the name A is used in an expression context to refer to the local variable A and in a type context to refer to the class A.


3.5.1 Name hiding

The scope of an entity typically encompasses more program text than the declaration space of the entity. In particular, the scope of an entity may include declarations that introduce new declaration spaces containing entities of the same name. Such declarations cause the original entity to become hidden. Conversely, an entity is said to be visible when it is not hidden.

Name hiding occurs when scopes overlap through nesting and when scopes overlap through inheritance. The characteristics of the two types of hiding are described in the following sections.


3.5.1.1 Hiding through nesting

Name hiding through nesting can occur as a result of nesting namespaces or types within namespaces, as a result of nesting types within classes or structs, and as a result of parameter and local variable declarations. Name hiding through nesting of scopes always occurs “silently”, i.e. no errors or warnings are reported when outer names are hidden by inner names.

In the example

class A
{
   int i = 0;
   void F() {
      int i = 1;
   }
   void G() {
      i = 1;
   }
}

within the F method, the instance variable i is hidden by the local variable i, but within the G method, i still refers to the instance variable.

When a name in an inner scope hides a name in an outer scope, it hides all overloaded occurrences of that name. In the example

class Outer
{
   static void F(int i) {}
   static void F(string s) {}
   class Inner
   {
      void G() {
         F(1);            // Invokes Outer.Inner.F
         F("Hello");      // Error
      }
      static void F(long l) {}
   }
}

the call F(1) invokes the F declared in Inner because all outer occurrences of F are hidden by the inner declaration. For the same reason, the call F("Hello") is in error.


3.5.1.2 Hiding through inheritance

Name hiding through inheritance occurs when classes or structs redeclare names that were inherited from base classes. This type of name hiding takes one of the following forms:

  • A constant, field, property, event, or type introduced in a class or struct hides all base class members with the same name.
  • A method introduced in a class or struct hides all non-method base class members with the same name, and all base class methods with the same signature (method name and parameter count, modifiers, and types).
  • An indexer introduced in a class or struct hides all base class indexers with the same signature (parameter count and types).

The rules governing operator declarations (§10.9) make it impossible for a derived class to declare an operator with the same signature as an operator in a base class. Thus, operators never hide one another.

Contrary to hiding a name from an outer scope, hiding an accessible name from an inherited scope causes a warning to be reported. In the example

class Base
{
   public void F() {}
}
class Derived: Base
{
   public void F() {}      // Warning, hiding an inherited name
}

the declaration of F in Derived causes a warning to be reported. Hiding an inherited name is specifically not an error, since that would preclude separate evolution of base classes. For example, the above situation might have come about because a later version of Base introduced an F method that wasn’t present in an earlier version of the class. Had the above situation been an error, then any change made to a base class in a separately versioned class library could potentially cause derived classes to become invalid.

The warning caused by hiding an inherited name can be eliminated through use of the new modifier:

class Base
{
   public void F() {}
}
class Derived: Base
{
   new public void F() {}
}

The new modifier indicates that the F in Derived is “new”, and that it is indeed intended to hide the inherited member.

A declaration of a new member hides an inherited member only within the scope of the new member.

class Base
{
   public static void F() {}
}
class Derived: Base
{
   new private static void F() {}   // Hides Base.F in Derived only
}
class MoreDerived: Derived
{
   static void G() { F(); }         // Invokes Base.F
}

In the example above, the declaration of F in Derived hides the F that was inherited from Base, but since the new F in Derived has private access, its scope does not extend to MoreDerived. Thus, the call F() in MoreDerived.G is valid and will invoke Base.F.


3.6 Namespace and type names

Several contexts in a C# program require a namespace-name or a type-name to be specified. Either form of name is written as one or more identifiers separated by “.” tokens.

namespace-name:
namespace-or-type-name
type-name:
namespace-or-type-name
namespace-or-type-name:
identifier
namespace-or-type-name . identifier

A type-name is a namespace-or-type-name that refers to a type. Following resolution as described below, the namespace-or-type-name of a type-name must refer to a type, or otherwise an error occurs.

A namespace-name is a namespace-or-type-name that refers to a namespace. Following resolution as described below, the namespace-or-type-name of a namespace-name must refer to a namespace, or otherwise an error occurs.

The meaning of a namespace-or-type-name is determined as follows:

  • If the namespace-or-type-name consists of a single identifier:
    • If the namespace-or-type-name appears within the body of a class or struct declaration, then starting with that class or struct declaration and continuing with each enclosing class or struct declaration (if any), if a member with the given name exists, is accessible, and denotes a type, then the namespace-or-type-name refers to that member. Note that non-type members (constructors, constants, fields, methods, properties, indexers, and operators) are ignored when determining the meaning of a namespace-or-type-name.
    • Otherwise, starting with the namespace declaration in which the namespace-or-type-name occurs (if any), continuing with each enclosing namespace declaration (if any), and ending with the global namespace, the following steps are evaluated until an entity is located:
      • If the namespace contains a namespace member with the given name, then the namespace-or-type-name refers to that member and, depending on the member, is classified as a namespace or a type.
      • Otherwise, if the namespace declaration contains a using-alias-directive that associates the given name with an imported namespace or type, then the namespace-or-type-name refers to that namespace or type.
      • Otherwise, if the namespaces imported by the using-namespace-directives of the namespace declaration contain exactly one type with the given name, then the namespace-or-type-name refers to that type.
      • Otherwise, if the namespaces imported by the using-namespace-directives of the namespace declaration contain more than one type with the given name, then the namespace-or-type-name is ambiguous and an error occurs.
    • Otherwise, the namespace-or-type-name is undefined and an error occurs.
  • Otherwise, the namespace-or-type-name is of the form N.I, where N is a namespace-or-type-name consisting of all identifiers but the rightmost one, and I is the rightmost identifier. N is first resolved as a namespace-or-type-name. If the resolution of N is not successful, an error occurs. Otherwise, N.I is resolved as follows:
    • If N is a namespace and I is the name of an accessible member of that namespace, then N.I refers to that member and, depending on the member, is classified as a namespace or a type.
    • If N is a class or struct type and I is the name of an accessible type in N, then N.I refers to that type.
    • Otherwise, N.I is an invalid namespace-or-type-name, and an error occurs.


3.6.1 Fully qualified names

Every namespace and type has a fully qualified name which uniquely identifies the namespace or type amongst all others. The fully qualified name of a namespace or type N is determined as follows:

  • If N is a member of the global namespace, its fully qualified name is N.
  • Otherwise, its fully qualified name is S.N, where S is the fully qualified name of the namespace or type in which N is declared.

In other words, the fully qualified name of N is the complete hierarchical path of identifiers that lead to N, starting from the global namespace. Because every member of a namespace or type must have a unique name, it follows that the fully qualified name of a namespace or type is always unique.

The example below shows several namespace and type declarations along with their associated fully qualified names.

class A {}            // A
namespace X            // X
{
   class B            // X.B
   {
      class C {}      // X.B.C
   }
   namespace Y         // X.Y
   {
      class D {}      // X.Y.D
   }
}
namespace X.Y         // X.Y
{
   class E {}         // X.Y.E
}


4. Types

The types of the C# language are divided into three categories: Value types, reference types, and pointer types.

type:
value-type
reference-type
pointer-type

Pointer types can be used only in unsafe code, and are discussed further in §18.2.

Value types differ from reference types in that variables of the value types directly contain their data, whereas variables of the reference types store references to their data, the latter known as objects. With reference types, it is possible for two variables to reference the same object, and thus possible for operations on one variable to affect the object referenced by the other variable. With value types, the variables each have their own copy of the data, and it is not possible for operations on one to affect the other.

C#’s type system is unified such that a value of any type can be treated as an object. Every type in C# directly or indirectly derives from the object class type, and object is the ultimate base class of all types. Values of reference types are treated as objects simply by viewing the values as type object. Values of value types are treated as objects by performing boxing and unboxing operations (§4.3).


4.1 Value types

A value type is either a struct type or an enumeration type. C# provides a set of predefined struct types called the simple types. The simple types are identified through reserved words, and are further subdivided into numeric types, integral types, and floating-point types.

value-type:
struct-type
enum-type
struct-type:
type-name
simple-type
simple-type:
numeric-type
bool
numeric-type:
integral-type
floating-point-type
decimal
integral-type:
sbyte
byte
short
ushort
int
uint
long
ulong
char
floating-point-type:
float
double
enum-type:
type-name

All value types implicitly inherit from class object. It is not possible for any type to derive from a value type, and value types are thus implicitly sealed.

A variable of a value type always contains a value of that type. Unlike reference types, it is not possible for a value of a value type to be null or to reference an object of a more derived type.

Assignment to a variable of a value type creates a copy of the value being assigned. This differs from assignment to a variable of a reference type, which copies the reference but not the object identified by the reference.


4.1.1 Default constructors

All value types implicitly declare a public parameterless constructor called the default constructor. The default constructor returns a zero-initialized instance known as the default value for the value type:

  • For all simple-types, the default value is the value produced by a bit pattern of all zeros:
    • For sbyte, byte, short, ushort, int, uint, long, and ulong, the default value is 0.
    • For char, the default value is '\x0000'.
    • For float, the default value is 0.0f.
    • For double, the default value is 0.0d.
    • For decimal, the default value is 0.0m.
    • For bool, the default value is false.
  • For an enum-type E, the default value is 0.
  • For a struct-type, the default value is the value produced by setting all value type fields to their default value and all reference type fields to null.

Like any other constructor, the default constructor of a value type is invoked using the new operator. In the example below, the i and j variables are both initialized to zero.

class A
{
   void F() {
      int i = 0;
      int j = new int();
   }
}

Because every value type implicitly has a public parameterless constructor, it is not possible for a struct type to contain an explicit declaration of a parameterless constructor. A struct type is however permitted to declare parameterized constructors. For example

struct Point
{
   int x, y;
   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }
}

Given the above declaration, the statements

Point p1 = new Point();
Point p2 = new Point(0, 0);

both create a Point with x and y initialized to zero.


4.1.2 Struct types

A struct type is a value type that can declare constructors, constants, fields, methods, properties, indexers, operators, and nested types. Struct types are described in §11.


4.1.3 Simple types

C# provides a set of predefined struct types called the simple types. The simple types are identified through reserved words, but these reserved words are simply aliases for predefined struct types in the System namespace, as described in the table below.

Reserved word | Aliased type
sbyte | System.SByte
byte | System.Byte
short | System.Int16
ushort | System.UInt16
int | System.Int32
uint | System.UInt32
long | System.Int64
ulong | System.UInt64
char | System.Char
float | System.Single
double | System.Double
bool | System.Boolean
decimal | System.Decimal

A simple type and the struct type it aliases are completely indistinguishable. In other words, writing the reserved word byte is exactly the same as writing System.Byte, and writing System.Int32 is exactly the same as writing the reserved word int.

Because a simple type aliases a struct type, every simple type has members. For example, int has the members declared in System.Int32 and the members inherited from System.Object, and the following statements are permitted:

int i = int.MaxValue;         // System.Int32.MaxValue constant
string s = i.ToString();      // System.Int32.ToString() instance method
string t = 123.ToString();      // System.Int32.ToString() instance method

The simple types differ from other struct types in that they permit certain additional operations:

  • Most simple types permit values to be created by writing literals (§2.5.3). For example, 123 is a literal of type int and 'a' is a literal of type char. C# makes no provision for literals of other struct types, and values of other struct types are ultimately always created through constructors of those struct types.
  • When the operands of an expression are all simple type constants, it is possible for the compiler to evaluate the expression at compile time. Such an expression is known as a constant-expression (§7.15). Expressions involving operators defined by other struct types always imply run time evaluation.
  • Through const declarations it is possible to declare constants of the simple types (§10.3). It is not possible to have constants of other struct types, but a similar effect is provided by static readonly fields.
  • Conversions involving simple types can participate in evaluation of conversion operators defined by other struct types, but a user-defined conversion operator can never participate in evaluation of another user-defined operator (§6.4.2).


4.1.4 Integral types

C# supports nine integral types: sbyte, byte, short, ushort, int, uint, long, ulong, and char. The integral types have the following sizes and ranges of values:

  • The sbyte type represents signed 8-bit integers with values between –128 and 127.
  • The byte type represents unsigned 8-bit integers with values between 0 and 255.
  • The short type represents signed 16-bit integers with values between –32768 and 32767.
  • The ushort type represents unsigned 16-bit integers with values between 0 and 65535.
  • The int type represents signed 32-bit integers with values between –2147483648 and 2147483647.
  • The uint type represents unsigned 32-bit integers with values between 0 and 4294967295.
  • The long type represents signed 64-bit integers with values between –9223372036854775808 and 9223372036854775807.
  • The ulong type represents unsigned 64-bit integers with values between 0 and 18446744073709551615.
  • The char type represents unsigned 16-bit integers with values between 0 to 65535. The set of possible values for the char type corresponds to the Unicode character set.

The integral-type unary and binary operators always operate with signed 32-bit precision, unsigned 32-bit precision, signed 64-bit precision, or unsigned 64-bit precision:

  • For the unary + and ~ operators, the operand is converted to type T, where T is the first of int, uint, long, and ulong that can fully represent all possible values of the operand. The operation is then performed using the precision of type T, and the type of the result T.
  • For the unary – operator, the operand is converted to type T, where T is the first of int and long that can fully represent all possible values of the operand. The operation is then performed using the precision of type T, and the type of the result is T. The unary – operator cannot be applied to operands of type ulong.
  • For the binary +, –, *, /, %, &, ^, |, ==, !=, >, <, >=, and <= operators, the operands are converted to type T, where T is the first of int, uint, long, and ulong that can fully represent all possible values of each operand. The operation is then performed using the precision of type T, and the type of the result is T (or bool for the relational operators).
  • For the binary << and >> operators, the left operand is converted to type T, where T is the first of int, uint, long, and ulong that can fully represent all possible values of the operand. The operation is then performed using the precision of type T, and the type of the result T.

The char type is classified as an integral type, but it differs from the other integral types in two ways:

  • There are no implicit conversions from other types to the char type. In particular, even though the sbyte, byte, and ushort types have ranges of values that are fully representable using the char type, implicit conversions from sbyte, byte, or ushort to char do not exist.
  • Constants of the char type must be written as character-literals. Character constants can only be written as integer-literals in combination with a cast. For example, (char)10 is the same as '\x000A'.

The checked and unchecked operators and statements are used to control overflow checking for integral-type arithmetic operations and conversions (§7.5.12). In a checked context, an overflow produces a compile-time error or causes an OverflowException to be thrown. In an unchecked context, overflows are ignored and any high-order bits that do not fit in the destination type are discarded.


4.1.5 Floating point types

C# supports two floating point types: float and double. The float and double types are represented using the 32-bit single-precision and 64-bit double-precision IEEE 754 formats, which provide the following sets of values:

  • Positive zero and negative zero. In most situations, positive zero and negative zero behave identically as the simple value zero, but certain operations distinguish between the two.
  • Positive infinity and negative infinity. Infinities are produced by such operations as dividing a non-zero number by zero. For example 1.0 / 0.0 yields positive infinity, and –1.0 / 0.0 yields negative infinity.
  • The Not-a-Number value, often abbreviated NaN. NaN’s are produced by invalid floating-point operations, such as dividing zero by zero.
  • The finite set of non-zero values of the form s × m × 2e, where s is 1 or -1, and m and e are determined by the particular floating-point type: For float, 0 < m < 224 and -149 = e = 104, and for double, 0 < m < 253 and -1075 = e = 970.

The float type can represent values ranging from approximately 1.5 × 10-45 to 3.4 × 1038 with a precision of 7 digits.

The double type can represent values ranging from approximately 5.0 × 10-324 to 1.7 × 10308 with a precision of 15-16 digits.

If one of the operands of a binary operator is of a floating-point type, then the other operand must be of an integral type or a floating-point type, and the operation is evaluated as follows:

  • If one of the operands of is of an integral type, then that operand is converted to the floating-point type of the other operand.
  • Then, if either of the operands is of type double, the other operand is converted to double, the operation is performed using at least double range and precision, and the type of the result is double (or bool for the relational operators).
  • Otherwise, the operation is performed using at least float range and precision, and the type of the result is float (or bool for the relational operators).

The floating-point operators, including the assignment operators, never produce exceptions. Instead, in exceptional situations, floating-point operations produce zero, infinity, or NaN, as described below:

  • If the result of a floating-point operation is too small for the destination format, the result of the operation becomes positive zero or negative zero.
  • If the result of a floating-point operation is too large for the destination format, the result of the operation becomes positive infinity or negative infinity.
  • If a floating-point operation is invalid, the result of the operation becomes NaN.
  • If one or both operands of a floating-point operation is NaN, the result of the operation becomes NaN.

Floating-point operations may be performed with higher precision than the result type of the operation. For example, some hardware architectures support an “extended” or “long double” floating-point type with greater range and precision than the double type, and implicitly perform all floating-point operations using this higher precision type. Only at excessive cost in performance can such hardware architectures be made to perform floating-point operations with less precision, and rather than require an implementation to forfeit both performance and precision, C# allows a higher precision type to be used for all floating-point operations. Other than delivering more precise results, this rarely has any measurable effects. However, in expressions of the form x * y / z, where the multiplication produces a result that is outside the double range, but the subsequent division brings the temporary result back into the double range, the fact that the expression is evaluated in a higher range format may cause a finite result to be produced instead of an infinity.


4.1.6 The decimal type

The decimal type is a 128-bit data type suitable for financial and monetary calculations. The decimal type can represent values ranging from 1.0 × 10-28 to approximately 7.9 × 1028 with 28-29 significant digits.

The finite set of values of type decimal are of the form s × m × 10e, where s is 1 or –1, 0 = m < 296, and -28 = e = 0. The decimal type does not support signed zeros, infinities, or NaN's.

A decimal is represented as a 96-bit integer scaled by a power of ten. For decimals with an absolute value less than 1.0m, the value is exact to the 28th decimal place, but no further. For decimals with an absolute value greater than or equal to 1.0m, the value is exact to 28 or 29 digits. Contrary to the float and double data types, decimal fractional numbers such as 0.1 can be represented exactly in the decimal representation. In the float and double representations, such numbers are often infinite fractions, making those representations more prone to round-off errors.

If one of the operands of a binary operator is of type decimal, then the other operand must be of an integral type or of type decimal. If an integral type operand is present, it is converted to decimal before the operation is performed.

Operations on values of type decimal are exact to 28 or 29 digits, but to no more than 28 decimal places. Results are rounded to the nearest representable value, and, when a result is equally close to two representable values, to the value that has an even number in the least significant digit position.

If a decimal arithmetic operation produces a value that is too small for the decimal format after rounding, the result of the operation becomes zero. If a decimal arithmetic operation produces a result that is too large for the decimal format, an OverflowException is thrown.

The decimal type has greater precision but smaller range than the floating-point types. Thus, conversions from the floating-point types to decimal might produce overflow exceptions, and conversions from decimal to the floating-point types might cause loss of precision. For these reasons, no implicit conversions exist between the floating-point types and decimal, and without explicit casts, it is not possible to mix floating-point and decimal operands in the same expression.


4.1.7 The bool type

The bool type represents boolean logical quantities. The possible values of type bool are true and false.

No standard conversions exist between bool and other types. In particular, the bool type is distinct and separate from the integral types, and a bool value cannot be used in place of an integral value, nor vice versa.

In the C and C++ languages, a zero integral value or a null pointer can be converted to the boolean value false, and a non-zero integral value or a non-null pointer can be converted to the boolean value true. In C#, such conversions are accomplished by explicitly comparing an integral value to zero or explicitly comparing an object reference to null.


4.1.8 Enumeration types

An enumeration type is a distinct type with named constants. Every enumeration type has an underlying type, which is byte, short, int, or long. Enumeration types are defined through enumeration declarations (§14.1).


4.2 Reference types

A reference type is a class type, an interface type, an array type, or a delegate type.

reference-type:
class-type
interface-type
array-type
delegate-type
class-type:
type-name
object
string
interface-type:
type-name
array-type:
non-array-type rank-specifiers
non-array-type:
type
rank-specifiers:
rank-specifier
rank-specifiers rank-specifier
rank-specifier:
[ dim-separatorsopt ]
dim-separators:
,
dim-separators ,
delegate-type:
type-name

A reference type value is a reference to an instance of the type, the latter known as an object. The special value null is compatible with all reference types and indicates the absence of an instance.


4.2.1 Class types

A class type defines a data structure that contains data members (constants, fields, and events), function members (methods, properties, indexers, operators, constructors, and destructors), and nested types. Class types support inheritance, a mechanism whereby derived classes can extend and specialize base classes. Instances of class types are created using object-creation-expressions (§7.5.10.1).

Class types are described in §10.


4.2.2 The object type

The object class type is the ultimate base class of all other types. Every type in C# directly or indirectly derives from the object class type.

The object keyword is simply an alias for the predefined System.Object class. Writing the keyword object is exactly the same as writing System.Object, and vice versa.


4.2.3 The string type

The string type is a sealed class type that inherits directly from object. Instances of the string class represent Unicode character strings.

Values of the string type can be written as string literals (§2.5.3).

The string keyword is simply an alias for the predefined System.String class. Writing the keyword string is exactly the same as writing System.String, and vice versa.


4.2.4 Interface types

An interface defines a contract. A class or struct that implements an interface must adhere to its contract. An interface may inherit from multiple base interfaces, and a class or struct may implement multiple interfaces.

Interface types are described in §13.


4.2.5 Array types

An array is a data structure that contains a number of variables which are accessed through computed indices. The variables contained in an array, also called the elements of the array, are all of the same type, and this type is called the element type of the array.

Array types are described in §12.


4.2.6 Delegate types

A delegate is a data structure that refers to a static method or to an object instance and an instance method of that object.

The closest equivalent of a delegate in C or C++ is a function pointer, but whereas a function pointer can only reference static functions, a delegate can reference both static and instance methods. In the latter case, the delegate stores not only a reference to the method’s entry point, but also a reference to the object instance for which to invoke the method.

Delegate types are described in §15.


4.3 Boxing and unboxing

Boxing and unboxing is a central concept in C#’s type system. It provides a binding link between value-types and reference-types by permitting any value of a value-type to be converted to and from type object. Boxing and unboxing enables a unified view of the type system wherein a value of any type can ultimately be treated as an object.


4.3.1 Boxing conversions

A boxing conversion permits any value-type to be implicitly converted to the type object or to any interface-type implemented by the value-type. Boxing a value of a value-type consists of allocating an object instance and copying the value-type value into that instance.

The actual process of boxing a value of a value-type is best explained by imagining the existence of a boxing class for that type. For any value-type T, the boxing class would be declared as follows:

class T_Box
{
   T value;
   T_Box(T t) {
      value = t;
   }
}

Boxing of a value v of type T now consists of executing the expression new T_Box(v), and returning the resulting instance as a value of type object. Thus, the statements

int i = 123;
object box = i;

conceptually correspond to

int i = 123;
object box = new int_Box(i);

Boxing classes like T_Box and int_Box above don’t actually exist and the dynamic type of a boxed value isn’t actually a class type. Instead, a boxed value of type T has the dynamic type T, and a dynamic type check using the is operator can simply reference type T. For example,

int i = 123;
object box = i;
if (box is int) {
   Console.Write("Box contains an int");
}

will output the string “Box contains an int” on the console.

A boxing conversion implies making a copy of the value being boxed. This is different from a conversion of a reference-type to type object, in which the value continues to reference the same instance and simply is regarded as the less derived type object. For example, given the declaration

struct Point
{
   public int x, y;
   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }
}

the following statements

Point p = new Point(10, 10);
object box = p;
p.x = 20;
Console.Write(((Point)box).x);

will output the value 10 on the console because the implicit boxing operation that occurs in the assignment of p to box causes the value of p to be copied. Had Point instead been declared a class, the value 20 would be output because p and box would reference the same instance.


4.3.2 Unboxing conversions

An unboxing conversion permits an explicit conversion from type object to any value-type or from any interface-type to any value-type that implements the interface-type. An unboxing operation consists of first checking that the object instance is a boxed value of the given value-type, and then copying the value out of the instance.

Referring to the imaginary boxing class described in the previous section, an unboxing conversion of an object box to a value-type T consists of executing the expression ((T_Box)box).value. Thus, the statements

object box = 123;
int i = (int)box;

conceptually correspond to

object box = new int_Box(123);
int i = ((int_Box)box).value;

For an unboxing conversion to a given value-type to succeed at run-time, the value of the source argument must be a reference to an object that was previously created by boxing a value of that value-type. If the source argument is null or a reference to an incompatible object, an InvalidCastException is thrown.


5. Variables

Variables represent storage locations. Every variable has a type that determines what values can be stored in the variable. C# is a type-safe language, and the C# compiler guarantees that values stored in variables are always of the appropriate type. The value of a variable can be changed through assignment or through use of the ++ and -- operators.

A variable must be definitely assigned (§5.3) before its value can be obtained.

As described in the following sections, variables are either initially assigned or initially unassigned. An initially assigned variable has a well-defined initial value and is always considered definitely assigned. An initially unassigned variable has no initial value. For an initially unassigned variable to be considered definitely assigned at a certain location, an assignment to the variable must occur in every possible execution path leading to that location.


5.1 Variable categories

C# defines seven categories of variables: static variables, instance variables, array elements, value parameters, reference parameters, output parameters, and local variables. The sections that follow describe each of these categories.

In the example

class A
{
   public static int x;
   int y;
   void F(int[] v, int a, ref int b, out int c) {
      int i = 1;
      c = a + b++;
   }
}

x is a static variable, y is an instance variable, v[0] is an array element, a is a value parameter, b is a reference parameter, c is an output parameter, and i is a local variable.


5.1.1 Static variables

A field declared with the static modifier is called a static variable. A static variable comes into existence when the type in which it is declared is loaded, and ceases to exist when the type in which it is declared is unloaded.

The initial value of a static variable is the default value (§5.2) of the variable’s type.

For purposes of definite assignment checking, a static variable is considered initially assigned.


5.1.2 Instance variables

A field declared without the static modifier is called an instance variable.


5.1.2.1 Instance variables in classes

An instance variable of a class comes into existence when a new instance of that class is created, and ceases to exist when there are no references to that instance and the destructor of the instance has executed.

The initial value of an instance variable of a class is the default value (§5.2) of the variable’s type.

For purposes of definite assignment checking, an instance variable of a class is considered initially assigned.


5.1.2.2 Instance variables in structs

An instance variable of a struct has exactly the same lifetime as the struct variable to which it belongs. In other words, when a variable of a struct type comes into existence or ceases to exist, so too do the instance variables of the struct.

The initial assignment state of an instance variable of a struct in the same as that of the containing struct variable. In other words, when a struct variable is considered initially assigned, so too are its instance variables, and when a struct variable is considered initially unassigned, its instance variables are likewise unassigned.


5.1.3 Array elements

The elements of an array come into existence when an array instance is created, and cease to exist when there are no references to that array instance.

The initial value of each of the elements of an array is the default value (§5.2) of the type of the array elements.

For purposes of definite assignment checking, an array element is considered initially assigned.


5.1.4 Value parameters

A parameter declared without a ref or out modifier is a value parameter.

A value parameter comes into existence upon invocation of the function member (method, constructor, accessor, or operator) to which the parameter belongs, and is initialized with the value of the argument given in the invocation. A value parameter ceases to exist upon return of the function member.

For purposes of definite assignment checking, a value parameter is considered initially assigned.


5.1.5 Reference parameters

A parameter declared with a ref modifier is a reference parameter.

A reference parameter does not create a new storage location. Instead, a reference parameter represents the same storage location as the variable given as the argument in the function member invocation. Thus, the value of a reference parameter is always the same as the underlying variable.

The following definite assignment rules apply to reference parameters. Note the different rules for output parameters described in §5.1.6.

  • A variable must be definitely assigned (§5.3) before it can be passed as a reference parameter in a function member invocation.
  • Within a function member, a reference parameter is considered initially assigned.

Within an instance method or instance accessor of a struct type, the this keyword behaves exactly as a reference parameter of the struct type (§7.5.7).


5.1.6 Output parameters

A parameter declared with an out modifier is an output parameter.

An output parameter does not create a new storage location. Instead, an output parameter represents the same storage location as the variable given as the argument in the function member invocation. Thus, the value of an output parameter is always the same as the underlying variable.

The following definite assignment rules apply to output parameters. Note the different rules for reference parameters described in §5.1.5.

  • A variable need not be definitely assigned before it can be passed as an output parameter in a function member invocation.
  • Following a function member invocation, each variable that was passed as an output parameter is considered assigned in that execution path.
  • Within a function member, an output parameter is considered initially unassigned.
  • Every output parameter of a function member must be definitely assigned (§5.3) before the function member returns.

Within a constructor of a struct type, the this keyword behaves exactly as an output parameter of the struct type (§7.5.7).


5.1.7 Local variables

A local variable is declared by a local-variable-declaration, which may occur in a block, a for-statement, or a switch-statement. A local variable comes into existence when control enters the block, for-statement, or switch-statement that immediately contains the local variable declaration. A local variable ceases to exist when control leaves its immediately containing block, for-statement, or switch-statement.

A local variable is not automatically initialized and thus has no default value. For purposes of definite assignment checking, a local variable is considered initially unassigned. A local-variable-declaration may include a variable-initializer, in which case the variable is considered definitely assigned in its entire scope, except within the expression provided in the variable-initializer.

Within the scope of a local variable, it is an error to refer to the local variable in a textual position that precedes its variable-declarator.


5.2 Default values

The following categories of variables are automatically initialized to their default values:

  • Static variables.
  • Instance variables of class instances.
  • Array elements.

The default value of a variable depends on the type of the variable and is determined as follows:

  • For a variable of a value-type, the default value is the same as the value computed by the value-type’s default constructor (§4.1.1).
  • For a variable of a reference-type, the default value is null.


5.3 Definite assignment

At a given location in the executable code of a function member, a variable is said to be definitely assigned if the compiler can prove, by static flow analysis, that the variable has been automatically initialized or has been the target of at least one assignment. The rules of definite assignment are:

  • An initially assigned variable (§5.3.1) is always considered definitely assigned.
  • An initially unassigned variable (§5.3.2) is considered definitely assigned at a given location if all possible execution paths leading to that location contain at least one of the following:
    • A simple assignment (§7.13.1) in which the variable is the left operand.
    • An invocation expression (§7.5.5) or object creation expression (§7.5.10.1) that passes the variable as an output parameter.
    • For a local variable, a local variable declaration (§8.5) that includes a variable initializer.

The definite assignment state of instance variables of a struct-type variable are tracked individually as well as collectively. In additional to the rules above, the following rules apply to struct-type variables and their instance variables:

  • An instance variable is considered definitely assigned if its containing struct-type variable is considered definitely assigned.
  • A struct-type variable is considered definitely assigned if each of its instance variables are considered definitely assigned.

Definite assignment is a requirement in the following contexts:

  • A variable must be definitely assigned at each location where its value is obtained. This ensures that undefined values never occur. The occurrence of a variable in an expression is considered to obtain the value of the variable, except when
    • the variable is the left operand of a simple assignment,
    • the variable is passed as an output parameter, or
    • the variable is a struct-type variable and occurs as the left operand of a member access.
  • A variable must be definitely assigned at each location where it is passed as a reference parameter. This ensures that the function member being invoked can consider the reference parameter initially assigned.
  • All output parameters of a function member must be definitely assigned at each location where the function member returns (through a return statement or through execution reaching the end of the function member body). This ensures that function members do no return undefined values in output parameters, thus enabling the compiler to consider a function member invocation that takes a variable as an output parameter equivalent to an assignment to the variable.
  • The this variable of a struct-type constructor must be definitely assigned at each location where the constructor returns.

The following example demonstrates how the different blocks of a try statement affect definite assignment.

class A
{
   static void F() {
      int i, j;
      try {
         // neither i nor j definitely assigned
         i = 1;
         // i definitely assigned
         j = 2;
         // i and j definitely assigned
      }
      catch {
         // neither i nor j definitely assigned
         i = 3;
         // i definitely assigned
      }
      finally {
         // neither i nor j definitely assigned
         i = 4;
         // i definitely assigned
         j = 5;
         // i and j definitely assigned
      }
      // i and j definitely assigned
   }
}

The static flow analysis performed to determine the definite assignment state of a variable takes into account the special behavior of the &&, ||, and ?: operators. In each of the methods in the example

class A
{
   static void F(int x, int y) {
      int i;
      if (x >= 0 && (i = y) >= 0) {
         // i definitely assigned
      }
      else {
         // i not definitely assigned
      }
      // i not definitely assigned
   }
   static void G(int x, int y) {
      int i;
      if (x >= 0 || (i = y) >= 0) {
         // i not definitely assigned
      }
      else {
         // i definitely assigned
      }
      // i not definitely assigned
   }
}

the variable i is considered definitely assigned in one of the embedded statements of an if statement but not in the other. In the if statement in the F method, the variable i is definitely assigned in the first embedded statement because execution of the expression (i = y) always precedes execution of this embedded statement. In contrast, the variable i is not definitely assigned in the second embedded statement since the variable i may be unassigned. Specifically, the variable i is unassigned if the value of the variable x is negative. Similarly, in the G method, the variable i is definitely assigned in the second embedded statement but not in the first embedded statement.


5.3.1 Initially assigned variables

The following categories of variables are classified as initially assigned:

  • Static variables.
  • Instance variables of class instances.
  • Instance variables of initially assigned struct variables.
  • Array elements.
  • Value parameters.
  • Reference parameters.


5.3.2 Initially unassigned variables

The following categories of variables are classified as initially unassigned:

  • Instance variables of initially unassigned struct variables.
  • Output parameters, including the this variable of struct constructors.
  • Local variables.


5.4 Variable references

A variable-reference is an expression that is classified as a variable. A variable-reference denotes a storage location that can be accessed both to fetch the current value and to store a new value. In C and C++, a variable-reference is known as an lvalue.

variable-reference:
expression

The following constructs require an expression to be a variable-reference:

  • The left hand side of an assignment (which may also be a property access or an indexer access).
  • An argument passed as a ref or out parameter in a method or constructor invocation.


6. Conversions

© 1987-2000 Microsoft Corporation. All rights reserved.


6.1 Implicit conversions

The following conversions are classified as implicit conversions:

  • Identity conversions
  • Implicit numeric conversions
  • Implicit enumeration conversions.
  • Implicit reference conversions
  • Boxing conversions
  • Implicit constant expression conversions
  • User-defined implicit conversions

Implicit conversions can occur in a variety of situations, including function member invocations (§7.4.3), cast expressions (§7.6.8), and assignments (§7.13).

The pre-defined implicit conversions always succeed and never cause exceptions to be thrown. Properly designed user-defined implicit conversions should exhibit these characteristics as well.


6.1.1 Identity conversion

An identity conversion converts from any type to the same type. This conversion exists only such that an entity that already has a required type can be said to be convertible to that type.


6.1.2 Implicit numeric conversions

The implicit numeric conversions are:

  • From sbyte to short, int, long, float, double, or decimal.
  • From byte to short, ushort, int, uint, long, ulong, float, double, or decimal.
  • From short to int, long, float, double, or decimal.
  • From ushort to int, uint, long, ulong, float, double, or decimal.
  • From int to long, float, double, or decimal.
  • From uint to long, ulong, float, double, or decimal.
  • From long to float, double, or decimal.
  • From ulong to float, double, or decimal.
  • From char to ushort, int, uint, long, ulong, float, double, or decimal.
  • From float to double.

Conversions from int, uint, or long to float and from long to double may cause a loss of precision, but will never cause a loss of magnitude. The other implicit numeric conversions never lose any information.

There are no implicit conversions to the char type. This in particular means that values of the other integral types do not automatically convert to the char type.


6.1.3 Implicit enumeration conversions

An implicit enumeration conversion permits the decimal-integer-literal 0 to be converted to any enum-type.


6.1.4 Implicit reference conversions

The implicit reference conversions are:

  • From any reference-type to object.
  • From any class-type S to any class-type T, provided S is derived from T.
  • From any class-type S to any interface-type T, provided S implements T.
  • From any interface-type S to any interface-type T, provided S is derived from T.
  • From an array-type S with an element type SE to an array-type T with an element type TE, provided all of the following are true:
    • S and T differ only in element type. In other words, S and T have the same number of dimensions.
    • Both SE and TE are reference-types.
    • An implicit reference conversion exists from SE to TE.
  • From any array-type to System.Array.
  • From any delegate-type to System.Delegate.
  • From any array-type or delegate-type to System.ICloneable.
  • From the null type to any reference-type.

The implicit reference conversions are those conversions between reference-types that can be proven to always succeed, and therefore require no checks at run-time.

Reference conversions, implicit or explicit, never change the referential identity of the object being converted. In other words, while a reference conversion may change the type of a value, it never changes the value itself.


6.1.5 Boxing conversions

A boxing conversion permits any value-type to be implicitly converted to the type object or to any interface-type implemented by the value-type. Boxing a value of a value-type consists of allocating an object instance and copying the value-type value into that instance.

Boxing conversions are further described in §4.3.1.


6.1.6 Implicit constant expression conversions

An implicit constant expression conversion permits the following conversions:

  • A constant-expression (§7.15) of type int can be converted to type sbyte, byte, short, ushort, uint, or ulong, provided the value of the constant-expression is within the range of the destination type.
  • A constant-expression of type long can be converted to type ulong, provided the value of the constant-expression is not negative.


6.1.7 User-defined implicit conversions

A user-defined implicit conversion consists of an optional standard implicit conversion, followed by execution of a user-defined implicit conversion operator, followed by another optional standard implicit conversion. The exact rules for evaluating user-defined conversions are described in §6.4.3.


6.2 Explicit conversions

The following conversions are classified as explicit conversions:

  • All implicit conversions.
  • Explicit numeric conversions.
  • Explicit enumeration conversions.
  • Explicit reference conversions.
  • Explicit interface conversions.
  • Unboxing conversions.
  • User-defined explicit conversions.

Explicit conversions can occur in cast expressions (§7.6.8).

The explicit conversions are conversions that cannot be proved to always succeed, conversions that are known to possibly lose information, and conversions across domains of types sufficiently different to merit explicit notation.

The set explicit conversions includes all implicit conversions. This in particular means that redundant cast expressions are allowed.


6.2.1 Explicit numeric conversions

The explicit numeric conversions are the conversions from a numeric-type to another numeric-type for which an implicit numeric conversion (§6.1.2) does not already exist:

  • From sbyte to byte, ushort, uint, ulong, or char.
  • From byte to sbyte and char.
  • From short to sbyte, byte, ushort, uint, ulong, or char.
  • From ushort to sbyte, byte, short, or char.
  • From int to sbyte, byte, short, ushort, uint, ulong, or char.
  • From uint to sbyte, byte, short, ushort, int, or char.
  • From long to sbyte, byte, short, ushort, int, uint, ulong, or char.
  • From ulong to sbyte, byte, short, ushort, int, uint, long, or char.
  • From char to sbyte, byte, or short.
  • From float to sbyte, byte, short, ushort, int, uint, long, ulong, char, or decimal.
  • From double to sbyte, byte, short, ushort, int, uint, long, ulong, char, float, or decimal.
  • From decimal to sbyte, byte, short, ushort, int, uint, long, ulong, char, float, or double.

Because the explicit conversions include all implicit and explicit numeric conversions, it is always possible to convert from any numeric-type to any other numeric-type using a cast expression (§7.6.8).

The explicit numeric conversions possibly lose information or possibly cause exceptions to be thrown. An explicit numeric conversion is processed as follows:

  • For a conversion from an integral type to another integral type, the processing depends on the overflow checking context (§7.5.12) in which the conversion takes place:
    • In a checked context, the conversion succeeds if the source argument is within the range of the destination type, but throws an OverflowException if the source argument is outside the range of the destination type.
    • In an unchecked context, the conversion always succeeds, and simply consists of discarding the most significant bits of the source value.
  • For a conversion from float, double, or decimal to an integral type, the source value is rounded towards zero to the nearest integral value, and this integral value becomes the result of the conversion. If the resulting integral value is outside the range of the destination type, an OverflowException is thrown.
  • For a conversion from double to float, the double value is rounded to the nearest float value. If the double value is too small to represent as a float, the result becomes positive zero or negative zero. If the double value is too large to represent as a float, the result becomes positive infinity or negative infinity. If the double value is NaN, the result is also NaN.
  • For a conversion from float or double to decimal, the source value is converted to decimal representation and rounded to the nearest number after the 28th decimal place if required (§4.1.6). If the source value is too small to represent as a decimal, the result becomes zero. If the source value is NaN, infinity, or too large to represent as a decimal, an InvalidCastException is thrown.
  • For a conversion from decimal to float or double, the decimal value is rounded to the nearest double or float value. While this conversion may lose precision, it never causes an exception to be thrown.


6.2.2 Explicit enumeration conversions

The explicit enumeration conversions are:

  • From sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, or decimal to any enum-type.
  • From any enum-type to sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, or decimal.
  • From any enum-type to any other enum-type.

An explicit enumeration conversion between two types is processed by treating any participating enum-type as the underlying type of that enum-type, and then performing an implicit or explicit numeric conversion between the resulting types. For example, given an enum-type E with and underlying type of int, a conversion from E to byte is processed as an explicit numeric conversion (§6.2.1) from int to byte, and a conversion from byte to E is processed as an implicit numeric conversion (§6.1.2) from byte to int.


6.2.3 Explicit reference conversions

The explicit reference conversions are:

  • From object to any reference-type.
  • From any class-type S to any class-type T, provided S is a base class of T.
  • From any class-type S to any interface-type T, provided S is not sealed and provided S does not implement T.
  • From any interface-type S to any class-type T, provided T is not sealed or provided T implements S.
  • From any interface-type S to any interface-type T, provided S is not derived from T.
  • From an array-type S with an element type SE to an array-type T with an element type TE, provided all of the following are true:
    • S and T differ only in element type. In other words, S and T have the same number of dimensions.
    • Both SE and TE are reference-types.
    • An explicit reference conversion exists from SE to TE.
  • From System.Array to any array-type.
  • From System.Delegate to any delegate-type.
  • From System.ICloneable to any array-type or delegate-type.

The explicit reference conversions are those conversions between reference-types that require run-time checks to ensure they are correct.

For an explicit reference conversion to succeed at run-time, the value of the source argument must be null or the actual type of the object referenced by the source argument must be a type that can be converted to the destination type by an implicit reference conversion (§6.1.4). If an explicit reference conversion fails, an InvalidCastException is thrown.

Reference conversions, implicit or explicit, never change the referential identity of the object being converted. In other words, while a reference conversion may change the type of a value, it never changes the value itself.


6.2.4 Unboxing conversions

An unboxing conversion permits an explicit conversion from type object to any value-type or from any interface-type to any value-type that implements the interface-type. An unboxing operation consists of first checking that the object instance is a boxed value of the given value-type, and then copying the value out of the instance.

Unboxing conversions are further described in §4.3.2.


6.2.5 User-defined explicit conversions

A user-defined explicit conversion consists of an optional standard explicit conversion, followed by execution of a user-defined implicit or explicit conversion operator, followed by another optional standard explicit conversion. The exact rules for evaluating user-defined conversions are described in §6.4.4.


6.3 Standard conversions

The standard conversions are those pre-defined conversions that can occur as part of a user-defined conversion.


6.3.1 Standard implicit conversions

The following implicit conversions are classified as standard implicit conversions:

  • Identity conversions (§6.1.1)
  • Implicit numeric conversions (§6.1.2)
  • Implicit reference conversions (§6.1.4)
  • Boxing conversions (§6.1.5)
  • Implicit constant expression conversions (§6.1.6)

The standard implicit conversions specifically exclude user-defined implicit conversions.


6.3.2 Standard explicit conversions

The standard explicit conversions are all standard implicit conversions plus the subset of the explicit conversions for which an opposite standard implicit conversion exists. In other words, if a standard implicit conversion exists from a type A to a type B, then a standard explicit conversion exists from type A to type B and from type B to type A.


6.4 User-defined conversions

C# allows the pre-defined implicit and explicit conversions to be augmented by user-defined conversions. User-defined conversions are introduced by declaring conversion operators (§10.9.3) in class and struct types.


6.4.1 Permitted user-defined conversions

C# permits only certain user-defined conversions to be declared. In particular, it is not possible to redefine an already existing implicit or explicit conversion. A class or struct is permitted to declare a conversion from a source type S to a target type T only if all of the following are true:

  • S and T are different types.
  • Either S or T is the class or struct type in which the operator declaration takes place.
  • Neither S nor T is object or an interface-type.
  • T is not a base class of S, and S is not a base class of T.

The restrictions that apply to user-defined conversions are discussed further in §10.9.3.


6.4.2 Evaluation of user-defined conversions

A user-defined conversion converts a value from its type, called the source type, to another type, called the target type. Evaluation of a user-defined conversion centers on finding the most specific user-defined conversion operator for the particular source and target types. This determination is broken into several steps:

  • Finding the set of classes and structs from which user-defined conversion operators will be considered. This set consists of the source type and its base classes and the target type and its base classes (with the implicit assumptions that only classes and structs can declare user-defined operators, and that non-class types have no base classes).
  • From that set of types, determining which user-defined conversion operators are applicable. For a conversion operator to be applicable, it must be possible to perform a standard conversion (§6.3) from the source type to the argument type of the operator, and it must be possible to perform a standard conversion from the result type of the operator to the target type.
  • From the set of applicable user-defined operators, determining which operator is unambiguously the most specific. In general terms, the most specific operator is the operator whose argument type is “closest” to the source type and whose result type is “closest” to the target type. The exact rules for establishing the most specific user-defined conversion operator are defined in the following sections.

Once a most specific user-defined conversion operator has been identified, the actual execution of the user-defined conversion involves up to three steps:

  • First, if required, performing a standard conversion from the source type to the argument type of the user-defined conversion operator.
  • Next, invoking the user-defined conversion operator to perform the conversion.
  • Finally, if required, performing a standard conversion from the result type of the user-defined conversion operator to the target type.

Evaluation of a user-defined conversion never involves more than one user-defined conversion operator. In other words, a conversion from type S to type T will never first execute a user-defined conversion from S to X and then execute a user-defined conversion from X to T.

Exact definitions of evaluation of user-defined implicit or explicit conversions are given in the following sections. The definitions make use of the following terms:

  • If a standard implicit conversion (§6.3.1) exists from a type A to a type B, and if neither A nor B are interface-types, then A is said to be encompassed by B, and B is said to encompass A.
  • The most encompassing type in a set of types is the one type that encompasses all other types in the set. If no single type encompasses all other types, then the set has no most encompassing type. In more intuitive terms, the most encompassing type is the “largest” type in the set—the one type to which each of the other types can be implicitly converted.
  • The most encompassed type in a set of types is the one type that is encompassed by all other types in the set. If no single type is encompassed by all other types, then the set has no most encompassed type. In more intuitive terms, the most encompassed type is the “smallest” type in the set—the one type that can be implicitly converted to each of the other types.


6.4.3 User-defined implicit conversions

A user-defined implicit conversion from type S to type T is processed as follows:

  • Find the set of types, D, from which user-defined conversion operators will be considered. This set consists of S (if S is a class or struct), the base classes of S (if S is a class), T (if T is a class or struct), and the base classes of T (if T is a class).
  • Find the set of applicable user-defined conversion operators, U. This set consists of the user-defined implicit conversion operators declared by the classes or structs in D that convert from a type encompassing S to a type encompassed by T. If U is empty, the conversion is undefined and an error occurs.
  • Find the most specific source type, SX, of the operators in U:
    • If any of the operators in U convert from S, then SX is S.
    • Otherwise, SX is the most encompassed type in the combined set of source types of the operators in U. If no most encompassed type can be found, then the conversion is ambiguous and an error occurs.
  • Find the most specific target type, TX, of the operators in U:
    • If any of the operators in U convert to T, then TX is T.
    • Otherwise, TX is the most encompassing type in the combined set of target types of the operators in U. If no most encompassing type can be found, then the conversion is ambiguous and an error occurs.
  • If U contains exactly one user-defined conversion operator that converts from SX to TX, then this is the most specific conversion operator. If no such operator exists, or if more than one such operator exists, then the conversion is ambiguous and an error occurs. Otherwise, the user-defined conversion is applied:
    • If S is not SX, then a standard implicit conversion from S to SX is performed.
    • The most specific user-defined conversion operator is invoked to convert from SX to TX.
    • If TX is not T, then a standard implicit conversion from TX to T is performed.


6.4.4 User-defined explicit conversions

A user-defined explicit conversion from type S to type T is processed as follows:

  • Find the set of types, D, from which user-defined conversion operators will be considered. This set consists of S (if S is a class or struct), the base classes of S (if S is a class), T (if T is a class or struct), and the base classes of T (if T is a class).
  • Find the set of applicable user-defined conversion operators, U. This set consists of the user-defined implicit or explicit conversion operators declared by the classes or structs in D that convert from a type encompassing or encompassed by S to a type encompassing or encompassed by T. If U is empty, the conversion is undefined and an error occurs.
  • Find the most specific source type, SX, of the operators in U:
    • If any of the operators in U convert from S, then SX is S.
    • Otherwise, if any of the operators in U convert from types that encompass S, then SX is the most encompassed type in the combined set of source types of those operators. If no most encompassed type can be found, then the conversion is ambiguous and an error occurs.
    • Otherwise, SX is the most encompassing type in the combined set of source types of the operators in U. If no most encompassing type can be found, then the conversion is ambiguous and an error occurs.
  • Find the most specific target type, TX, of the operators in U:
    • If any of the operators in U convert to T, then TX is T.
    • Otherwise, if any of the operators in U convert to types that are encompassed by T, then TX is the most encompassing type in the combined set of source types of those operators. If no most encompassing type can be found, then the conversion is ambiguous and an error occurs.
    • Otherwise, TX is the most encompassed type in the combined set of target types of the operators in U. If no most encompassed type can be found, then the conversion is ambiguous and an error occurs.
  • If U contains exactly one user-defined conversion operator that converts from SX to TX, then this is the most specific conversion operator. If no such operator exists, or if more than one such operator exists, then the conversion is ambiguous and an error occurs. Otherwise, the user-defined conversion is applied:
    • If S is not SX, then a standard explicit conversion from S to SX is performed.
    • The most specific user-defined conversion operator is invoked to convert from SX to TX.
    • If TX is not T, then a standard explicit conversion from TX to T is performed.


7. Expressions

An expression is a sequence of operators and operands that specifies a computation. This chapter defines the syntax, order of evaluation, and meaning of expressions.


7.1 Expression classifications

An expression is classified as one of the following:

  • A value. Every value has an associated type.
  • A variable. Every variable has an associated type, namely the declared type of the variable.
  • A namespace. An expression with this classification can only appear as the left hand side of a member-access (§7.5.4). In any other context, an expression classified as a namespace causes an error.
  • A type. An expression with this classification can only appear as the left hand side of a member-access (§7.5.4). In any other context, an expression classified as a type causes an error.
  • A method group, which is a set of overloaded methods resulting from a member lookup (§7.3). A method group may have associated instance expression. When an instance method is invoked, the result of evaluating the instance expression becomes the instance represented by this (§7.5.7). A method group is only permitted in an invocation-expression (§7.5.5) or a delegate-creation-expression (§7.5.10.3). In any other context, an expression classified as a method group causes an error.
  • A property access. Every property access has an associated type, namely the type of the property. A property access may furthermore have an associated instance expression. When an accessor (the get or set block) of an instance property access is invoked, the result of evaluating the instance expression becomes the instance represented by this (§7.5.7).
  • An event access. Every event access has an associated type, namely the type of the event. An event access may furthermore have an associated instance expression. An event access may appear as the left hand operand of the += and -= operators (§7.13.3). In any other context, an expression classified as an event access causes an error.
  • An indexer access. Every indexer access has an associated type, namely the element type of the indexer. Furthermore, an indexer access has an associated instance expression and an associated argument list. When an accessor (the get or set block) of an indexer access is invoked, the result of evaluating the instance expression becomes the instance represented by this (§7.5.7), and the result of evaluating the argument list becomes the parameter list of the invocation.
  • Nothing. This occurs when the expression is an invocation of a method with a return type of void. An expression classified as nothing is only valid in the context of a statement-expression (§8.6).

The final result of an expression is never a namespace, type, method group, or event access. Rather, as noted above, these categories of expressions are intermediate constructs that are only permitted in certain contexts.

A property access or indexer access is always reclassified as a value by performing an invocation of the get-accessor or the set-accessor. The particular accessor is determined by the context of the property or indexer access: If the access is the target of an assignment, the set-accessor is invoked to assign a new value (§7.13.1). Otherwise, the get-accessor is invoked to obtain the current value (§7.1.1).


7.1.1 Values of expressions

Most of the constructs that involve an expression ultimately require the expression to denote a value. In such cases, if the actual expression denotes a namespace, a type, a method group, or nothing, an error occurs. However, if the expression denotes a property access, an indexer access, or a variable, the value of the property, indexer, or variable is implicitly substituted:

  • The value of a variable is simply the value currently stored in the storage location identified by the variable. A variable must be considered definitely assigned (§5.3) before its value can be obtained, or otherwise a compile-time error occurs.
  • The value of a property access expression is obtained by invoking the get-accessor of the property. If the property has no get-accessor, an error occurs. Otherwise, a function member invocation (§7.4.3) is performed, and the result of the invocation becomes the value of the property access expression.
  • The value of an indexer access expression is obtained by invoking the get-accessor of the indexer. If the indexer has no get-accessor, an error occurs. Otherwise, a function member invocation (§7.4.3) is performed with the argument list associated with the indexer access expression, and the result of the invocation becomes the value of the indexer access expression.


7.2 Operators

Expressions are constructed from operands and operators. The operators of an expression indicate which operations to apply to the operands. Examples of operators include +, -, *, /, and new. Examples of operands include literals, fields, local variables, and expressions.

There are three types of operators:

  • Unary operators. The unary operators take one operand and use either prefix notation (such as –x) or postfix notation (such as x++).
  • Binary operators. The binary operators take two operands and all use infix notation (such as x + y).
  • Ternary operator. Only one ternary operator, ?:, exists. The ternary operator takes three operands and uses infix notation (c? x: y).

The order of evaluation of operators in an expression is determined by the precedence and associativity of the operators (§7.2.1).

Certain operators can be overloaded. Operator overloading permits user-defined operator implementations to be specified for operations where one or both of the operands are of a user-defined class or struct type (§7.2.2).


7.2.1 Operator precedence and associativity

When an expression contains multiple operators, the precedence of the operators control the order in which the individual operators are evaluated. For example, the expression x + y * z is evaluated as x + (y * z) because the * operator has higher precedence than the + operator. The precedence of an operator is established by the definition of its associated grammar production. For example, an additive-expression consists of a sequence of multiplicative-expressions separated by + or - operators, thus giving the + and - operators lower precedence than the *, /, and % operators.

The following table summarizes all operators in order of precedence from highest to lowest:

Section | Category | Operators
7.5 | Primary | (x) x.y f(x) a[x] x++ x-- new typeof sizeof checked unchecked
7.6 | Unary | + - ! ~ ++x --x (T)x
7.7 | Multiplicative | * / %
7.7 | Additive | + -
7.8 | Shift | << >>
7.9 | Relational | < > <= >= is
7.9 | Equality | == !=
7.10 | Logical AND | &
7.10 | Logical XOR | ^
7.10 | Logical OR | |
7.11 | Conditional AND | &&
7.11 | Conditional OR | ||
7.12 | Conditional | ?:
7.13 | Assignment | = *= /= %= += -= <<= >>= &= ^= |=

When an operand occurs between two operators with the same precedence, the associativity of the operators controls the order in which the operations are performed:

  • Except for the assignment operators, all binary operators are left-associative, meaning that operations are performed from left to right. For example, x + y + z is evaluated as (x + y) + z.
  • The assignment operators and the conditional operator (?:) are right-associative, meaning that operations are performed from right to left. For example, x = y = z is evaluated as x = (y = z).

Precedence and associativity can be controlled using parentheses. For example, x + y * z first multiplies y by z and then adds the result to x, but (x + y) * z first adds x and y and then multiplies the result by z.


7.2.2 Operator overloading

All unary and binary operators have predefined implementations that are automatically available in any expression. In addition to the predefined implementations, user-defined implementations can be introduced by including operator declarations in classes and structs (§10.9). User-defined operator implementations always take precedence over predefined operator implementations: Only when no applicable user-defined operator implementations exist will the predefined operator implementations be considered.

The overloadable unary operators are:

+   -   !   ~   ++   --   true   false

The overloadable binary operators are:

+   -   *   /   %   &   |   ^   <<   >>   ==   !=   >   <   >=   <=

Only the operators listed above can be overloaded. In particular, it is not possible to overload member access, method invocation, or the =, &&, ||, ?:, new, typeof, sizeof, and is operators.

When a binary operator is overloaded, the corresponding assignment operator is also implicitly overloaded. For example, an overload of operator * is also an overload of operator *=. This is described further in §7.13. Note that the assignment operator itself (=) cannot be overloaded. An assignment always performs a simple bit-wise copy of a value into a variable.

Cast operations, such as (T)x, are overloaded by providing user-defined conversions (§6.4).

Element access, such as a[x], is not considered an overloadable operator. Instead, user-defined indexing is supported through indexers (§10.8).

In expressions, operators are referenced using operator notation, and in declarations, operators are referenced using functional notation. The following table shows the relationship between operator and functional notations for unary and binary operators. In the first entry, op denotes any overloadable unary operator. In the second entry, op denotes the unary ++ and -- operators. In the third entry, op denotes any overloadable binary operator.

Operator notation | Functional notation
op x | operator op(x)
x op | operator op(x)
x op y | operator op(x, y)

User-defined operator declarations always require at least one of the parameters to be of the class or struct type that contains the operator declaration. Thus, it is not possible for a user-defined operator to have the same signature as a predefined operator.

User-defined operator declarations cannot modify the syntax, precedence, or associativity of an operator. For example, the * operator is always a binary operator, always has the precedence level specified in §7.2.1, and is always left-associative.

While it is possible for a user-defined operator to perform any computation it pleases, implementations that produce results other than those that are intuitively expected are strongly discouraged. For example, an implementation of operator == should compare the two operands for equality and return an appropriate result.

The descriptions of individual operators in §7.5 through §7.13 specify the predefined implementations of the operators and any additional rules that apply to each operator. The descriptions make use of the terms unary operator overload resolution, binary operator overload resolution, and numeric promotion, definitions of which are found in the following sections.


7.2.3 Unary operator overload resolution

An operation of the form op x or x op, where op is an overloadable unary operator, and x is an expression of type X, is processed as follows:

  • The set of candidate user-defined operators provided by X for the operation operator op(x) is determined using the rules of §7.2.5.
  • If the set of candidate user-defined operators is not empty, then this becomes the set of candidate operators for the operation. Otherwise, the predefined unary operator op implementations become the set of candidate operators for the operation. The predefined implementations of a given operator are specified in the description of the operator (§7.5 and §7.6).
  • The overload resolution rules of §7.4.2 are applied to the set of candidate operators to select the best operator with respect to the argument list (x), and this operator becomes the result of the overload resolution process. If overload resolution fails to select a single best operator, an error occurs.


7.2.4 Binary operator overload resolution

An operation of the form x op y, where op is an overloadable binary operator, x is an expression of type X, and y is an expression of type Y, is processed as follows:

  • The set of candidate user-defined operators provided by X and Y for the operation operator op(x, y) is determined. The set consists of the union of the candidate operators provided by X and the candidate operators provided by Y, each determined using the rules of §7.2.5. If X and Y are the same type, or if X and Y are derived from a common base type, then shared candidate operators only occur in the combined set once.
  • If the set of candidate user-defined operators is not empty, then this becomes the set of candidate operators for the operation. Otherwise, the predefined binary operator op implementations become the set of candidate operators for the operation. The predefined implementations of a given operator are specified in the description of the operator (§7.7 through §7.13).
  • The overload resolution rules of §7.4.2 are applied to the set of candidate operators to select the best operator with respect to the argument list (x, y), and this operator becomes the result of the overload resolution process. If overload resolution fails to select a single best operator, an error occurs.


7.2.5 Candidate user-defined operators

Given a type T and an operation operator op(A), where op is an overloadable operator and A is an argument list, the set of candidate user-defined operators provided by T for operator op(A) is determined as follows:

  • For all operator op declarations in T, if at least one operator is applicable (§7.4.2.1) with respect to the argument list A, then the set of candidate operators consists of all applicable operator op declarations in T.
  • Otherwise, if T is object, the set of candidate operators is empty.
  • Otherwise, the set of candidate operators provided by T is the set of candidate operators provided by the direct base class of T.


7.2.6 Numeric promotions

Numeric promotion consists of automatically performing certain implicit conversions of the operands of the predefined unary and binary numeric operators. Numeric promotion is not a distinct mechanism, but rather an effect of applying overload resolution to the predefined operators. Numeric promotion specifically does not affect evaluation of user-defined operators, although user-defined operators can be implemented to exhibit similar effects.

As an example of numeric promotion, consider the predefined implementations of the binary * operator:

int operator *(int x, int y);
uint operator *(uint x, uint y);
long operator *(long x, long y);
ulong operator *(ulong x, ulong y);
float operator *(float x, float y);
double operator *(double x, double y);
decimal operator *(decimal x, decimal y);

When overload resolution rules (§7.4.2) are applied to this set of operators, the effect is to select the first of the operators for which implicit conversions exist from the operand types. For example, for the operation b * s, where b is a byte and s is a short, overload resolution selects operator *(int, int) as the best operator. Thus, the effect is that b and s are converted to int, and the type of the result is int. Likewise, for the operation i * d, where i is an int and d is a double, overload resolution selects operator *(double, double) as the best operator.


7.2.6.1 Unary numeric promotions

Unary numeric promotion occurs for the operands of the predefined +, –, and ~ unary operators. Unary numeric promotion simply consists of converting operands of type sbyte, byte, short, ushort, or char to type int. Additionally, for the unary – operator, unary numeric promotion converts operands of type uint to type long.


7.2.6.2 Binary numeric promotions

Binary numeric promotion occurs for the operands of the predefined +, –, *, /, %, &, |, ^, ==, !=, >, <, >=, and <= binary operators. Binary numeric promotion implicitly converts both operands to a common type which, in case of the non-relational operators, also becomes the result type of the operation. Binary numeric promotion consists of applying the following rules, in the order they appear here:

  • If either operand is of type decimal, the other operand is converted to type decimal, or an error occurs if the other operand is of type float or double.
  • Otherwise, if either operand is of type double, the other operand is converted to type double.
  • Otherwise, if either operand is of type float, the other operand is converted to type float.
  • Otherwise, if either operand is of type ulong, the other operand is converted to type ulong, or an error occurs if the other operand is of type sbyte, short, int, or long.
  • Otherwise, if either operand is of type long, the other operand is converted to type long.
  • Otherwise, if either operand is of type uint and the other operand is of type sbyte, short, or int, both operands are converted to type long.
  • Otherwise, if either operand is of type uint, the other operand is converted to type uint.
  • Otherwise, both operands are converted to type int.

Note that the first rule disallows any operations that mix the decimal type with the double and float types. The rule follows from the fact that there are no implicit conversions between the decimal type and the double and float types.

Also note that it is not possible for an operand to be of type ulong when the other operand is of a signed integral type. The reason is that no integral type exists that can represent the full range of ulong as well as the signed integral types.

In both of the above cases, a cast expression can be used to explicitly convert one operand to a type that is compatible with the other operand.

In the example

decimal AddPercent(decimal x, double percent) {
   return x * (1.0 + percent / 100.0);
}

a compile-time error occurs because a decimal cannot be multiplied by a double. The error is resolved by explicitly converting the second operand to decimal:

decimal AddPercent(decimal x, double percent) {
   return x * (decimal)(1.0 + percent / 100.0);
}


7.3 Member lookup

A member lookup is the process whereby the meaning of a name in the context of a type is determined. A member lookup may occur as part of evaluating a simple-name (§7.5.2) or a member-access (§7.5.4) in an expression.

A member lookup of a name N in a type T is processed as follows:

  • First, the set of all accessible (§3.3) members named N declared in T and the base types (§7.3.1) of T is constructed. Declarations that include an override modifier are excluded from the set. If no members named N exist and are accessible, then the lookup produces no match, and the following steps are not evaluated.
  • Next, members that are hidden by other members are removed from the set. For every member S.M in the set, where S in the type in which the member M is declared, the following rules are applied:
    • If M is a constant, field, property, event, type, or enumeration member, then all members declared in a base type of S are removed from the set.
    • If M is a method, then all non-method members declared in a base type of S are removed from the set, and all methods with the same signature as M declared in a base type of S are removed from the set.
  • Finally, having removed hidden members, the result of the lookup is determined:
    • If the set consists of a single non-method member, then this member is the result of the lookup.
    • Otherwise, if the set contains only methods, then this group of methods is the result of the lookup.
    • Otherwise, the lookup is ambiguous, and a compile-time error occurs (this situation can only occur for a member lookup in an interface that has multiple direct base interfaces).

For member lookups in types other than interfaces, and member lookups in interfaces that are strictly single-inheritance (each interface in the inheritance chain has exactly zero or one direct base interface), the effect of the lookup rules is simply that derived members hide base members with the same name or signature. Such single-inheritance lookups are never ambiguous. The ambiguities that can possibly arise from member lookups in multiple-inheritance interfaces are described in §13.2.5.


7.3.1 Base types

For purposes of member lookup, a type T is considered to have the following base types:

  • If T is object, then T has no base type.
  • If T is a value-type, the base type of T is the class type object.
  • If T is a class-type, the base types of T are the base classes of T, including the class type object.
  • If T is an interface-type, the base types of T are the base interfaces of T and the class type object.
  • If T is an array-type, the base types of T are the class types System.Array and object.
  • If T is a delegate-type, the base types of T are the class types System.Delegate and object.


7.4 Function members

Function members are members that contain executable statements. Function members are always members of types and cannot be members of namespaces. C# defines the following five categories of function members:

  • Constructors
  • Methods
  • Properties
  • Indexers
  • User-defined operators

The statements contained in function members are executed through function member invocations. The actual syntax for writing a function member invocation depends on the particular function member category. However, all function member invocations are expressions, allow arguments to be passed to the function member, and allow the function member to compute and return a result.

The argument list (§7.4.1) of a function member invocation provides actual values or variable references for the parameters of the function member.

Invocations of constructors, methods, indexers, and operators employ overload resolution to determine which of a candidate set of function members to invoke. This process is described in §7.4.2.

Once a particular function member has been identified at compile-time, possibly through overload resolution, the actual run-time process of invoking the function member is described in §7.4.3.

The following table summarizes the processing that takes place in constructs involving the five categories of function members. In the table, e, x, y, and value indicate expressions classified as variables or values, T indicates an expression classified as a type, F is the simple name of a method, and P is the simple name of a property.

Construct | Example | Description
Constructor invocation | new T(x, y) | Overload resolution is applied to select the best constructor in the class or struct T. The constructor is invoked with the argument list (x, y).
Method invocation | F(x, y) | Overload resolution is applied to select the best method F in the containing class or struct. The method is invoked with the argument list (x, y). If the method is not static, the instance expression is this.
 | T.F(x, y) | Overload resolution is applied to select the best method F in the class or struct T. An error occurs if the method is not static. The method is invoked with the argument list (x, y).
 | e.F(x, y) | Overload resolution is applied to select the best method F in the class, struct, or interface given by the type of e. An error occurs if the method is static. The method is invoked with the instance expression e and the argument list (x, y).
Property access | P | The get accessor of the property P in the containing class or struct is invoked. An error occurs if P is write-only. If P is not static, the instance expression is this.
 | P = value | The set accessor of the property P in the containing class or struct is invoked with the argument list (value). An error occurs if P is read-only. If P is not static, the instance expression is this.
 | T.P | The get accessor of the property P in the class or struct T is invoked. An error occurs if P is not static or if P is write-only.
 | T.P = value | The set accessor of the property P in the class or struct T is invoked with the argument list (value). An error occurs if P is not static or if P is read-only.
 | e.P | The get accessor of the property P in the class, struct, or interface given by the type of e is invoked with the instance expression e. An error occurs if P is static or if P is write-only.
 | e.P = value | The set accessor of the property P in the class, struct, or interface given by the type of e is invoked with the instance expression e and the argument list (value). An error occurs if P is static or if P is read-only.
Indexer access | e[x, y] | Overload resolution is applied to select the best indexer in the class, struct, or interface given by the type of e. The get accessor of the indexer is invoked with the instance expression e and the argument list (x, y). An error occurs if the indexer is write-only.
 | e[x, y] = value | Overload resolution is applied to select the best indexer in the class, struct, or interface given by the type of e. The set accessor of the indexer is invoked with the instance expression e and the argument list (x, y, value). An error occurs if the indexer is read-only.
Operator invocation | -x | Overload resolution is applied to select the best unary operator in the class or struct given by the type of x. The selected operator is invoked with the argument list (x).
 | x + y | Overload resolution is applied to select the best binary operator in the classes or structs given by the types of x and y. The selected operator is invoked with the argument list (x, y).


7.4.1 Argument lists

Every function member invocation includes an argument list which provides actual values or variable references for the parameters of the function member. The syntax for specifying the argument list of a function member invocation depends on the function member category:

  • For constructors, methods, and delegates, the arguments are specified as an argument-list, as described below.
  • For properties, the argument list is empty when invoking the get accessor, and consists of the expression specified as the right operand of the assignment operator when invoking the set accessor.
  • For indexers, the argument list consists of the expressions specified between the square brackets in the indexer access. When invoking the set accessor, the argument list additionally includes the expression specified as the right operand of the assignment operator.
  • For user-defined operators, the argument list consists of the single operand of the unary operator or the two operands of the binary operator.

The arguments of properties, indexers, and user-defined operators are always passed as value parameters (§10.5.1.1). Reference and output parameters are not supported for these categories of function members.

The arguments of a constructor, method, or delegate invocation are specified as an argument-list:

argument-list:
argument
argument-list , argument
argument:
expression
ref variable-reference
out variable-reference

An argument-list consists of zero or more arguments, separated by commas. Each argument can take one of the following forms:

  • An expression, indicating that the argument is passed as a value parameter (§10.5.1.1).
  • The keyword ref followed by a variable-reference (§5.4), indicating that the argument is passed as a reference parameter (§10.5.1.2). A variable must be definitely assigned (§5.3) before it can be passed as a reference parameter.
  • The keyword out followed by a variable-reference (§5.4), indicating that the argument is passed as an output parameter (§10.5.1.3). A variable is considered definitely assigned (§5.3) following a function member invocation in which the variable is passed as an output parameter.

During the run-time processing of a function member invocation (§7.4.3), the expressions or variable references of an argument list are evaluated in order, from left to right, as follows:

  • For a value parameter, the argument expression is evaluated and an implicit conversion (§6.1) to the corresponding parameter type is performed. The resulting value becomes the initial value of the value parameter in the function member invocation.
  • For a reference or output parameter, the variable reference is evaluated and the resulting storage location becomes the storage location represented by the parameter in the function member invocation. If the variable reference given as a reference or output parameter is an array element of a reference-type, a run-time check is performed to ensure that element type of the array is identical to the type of the parameter. If this check fails, an ArrayTypeMismatchException is thrown.

Methods, indexers, and constructors may declare their last parameter to be a parameter array (§10.5.1.4). Such function members are invoked either in their normal form or in their expanded form depending on which is applicable (§7.4.2.1):

  • When a function member with a parameter array is invoked in its normal form, the argument given for the parameter array must be a single expression of a type that is implicitly convertible (§6.1) to the parameter array type. In this case, the parameter array acts precisely like a value parameter.
  • When a function member with a parameter array is invoked in its expanded form, the invocation must specify zero or more arguments for the parameter array, where each argument is an expression of a type that is implicitly convertible (§6.1) to the element type of the parameter array. In this case, the invocation creates an instance of the parameter array type with a length corresponding to the number of arguments, initializes the elements of the array instance with the given argument values, and uses the newly created array instance as the actual argument.

The expressions of an argument list are always evaluated in the order they are written. Thus, the example

class Test
{
   static void F(int x, int y, int z) {
      Console.WriteLine("x = {0}, y = {1}, z = {2}", x, y, z);
   }
   static void Main() {
      int i = 0;
      F(i++, i++, i++);
   }
}

produces the output

x = 0, y = 1, z = 2

The array co-variance rules (§12.5) permit a value of an array type A[] to be a reference to an instance of an array type B[], provided an implicit reference conversion exists from B to A. Because of these rules, when an array element of a reference-type is passed as a reference or output parameter, a run-time check is required to ensure that the actual element type of the array is identical to that of the parameter. In the example

class Test
{
   static void F(ref object x) {...}
   static void Main() {
      object[] a = new object[10];
      object[] b = new string[10];
      F(ref a[0]);      // Ok
      F(ref b[1]);      // ArrayTypeMismatchException
   }
}

the second invocation of F causes an ArrayTypeMismatchException to be thrown because the actual element type of b is string and not object.

When a function member with a parameter array is invoked in its expanded form, the invocation is processed exactly as if an array creation expression with an array initializer (§7.5.10.2) was inserted around the expanded parameters. For example, given the declaration

void F(int x, int y, params object[] args);

the following invocations of the expanded form of the method

F(10, 20);
F(10, 20, 30, 40);
F(10, 20, 1, "hello", 3.0);

correspond exactly to

F(10, 20, new object[] {});
F(10, 20, new object[] {30, 40});
F(10, 20, new object[] {1, "hello", 3.0});

Note in particular that an empty array is created when there are zero arguments given for the parameter array.


7.4.2 Overload resolution

Overload resolution is a mechanism for selecting the best function member to invoke given an argument list and a set of candidate function members. Overload resolution selects the function member to invoke in the following distinct contexts within C#:

  • Invocation of a method named in an invocation-expression (§7.5.5).
  • Invocation of a constructor named in an object-creation-expression (§7.5.10.1).
  • Invocation of an indexer accessor through an element-access (§7.5.6).
  • Invocation of a predefined or user-defined operator referenced in an expression (§7.2.3 and §7.2.4).

Each of these contexts defines the set of candidate function members and the list of arguments in its own unique way. However, once the candidate function members and the argument list have been identified, the selection of the best function member is the same in all cases:

  • First, the set of candidate function members is reduced to those function members that are applicable with respect to the given argument list (§7.4.2.1). If this reduced set is empty, an error occurs.
  • Then, given the set of applicable candidate function members, the best function member in that set is located. If the set contains only one function member, then that function member is the best function member. Otherwise, the best function member is the one function member that is better than all other function members with respect to the given argument list, provided that each function member is compared to all other function members using the rules in §7.4.2.2. If there is not exactly one function member that is better than all other function members, then the function member invocation is ambiguous and an error occurs.

The following sections define the exact meanings of the terms applicable function member and better function member.


7.4.2.1 Applicable function member

A function member is said to be an applicable function member with respect to an argument list A when all of the following are true:

  • The number of arguments in A is identical to the number of parameters in the function member declaration.
  • For each argument in A, the parameter passing mode of the argument is identical to the parameter passing mode of the corresponding parameter, and
    • for a value parameter or a parameter array, an implicit conversion (§6.1) exists from the type of the argument to the type of the corresponding parameter, or
    • for a ref or out parameter, the type of the argument is identical to the type of the corresponding parameter.

For a function member that includes a parameter array, if the function member is applicable by the above rules, it is said to be applicable in its normal form. If a function member that includes a parameter array is not applicable in its normal form, the function member may instead be applicable in its expanded form:

  • The expanded form is constructed by replacing the parameter array in the function member declaration with zero or more value parameters of the element type of the parameter array such that the number of arguments in the argument list A matches the total number of parameters. If A has fewer arguments than the number of fixed parameters in the function member declaration, the expanded form of the function member cannot be constructed and is thus not applicable.
  • If the class, struct, or interface in which the function member is declared already contains another function member with the same signature as the expanded form, the expanded form is not applicable.
  • Otherwise, the expanded form is applicable if for each argument in A the parameter passing mode of the argument is identical to the parameter passing mode of the corresponding parameter, and
    • for a fixed value parameter or a value parameter created by the expansion, an implicit conversion (§6.1) exists from the type of the argument to the type of the corresponding parameter, or
    • for a ref or out parameter, the type of the argument is identical to the type of the corresponding parameter.


7.4.2.2 Better function member

Given an argument list A with a set of argument types A1, A2, ..., AN and two applicable function members MP and MQ with parameter types P1, P2, ..., PN and Q1, Q2, ..., QN, MP is defined to be a better function member than MQ if

  • for each argument, the implicit conversion from AX to PX is not worse than the implicit conversion from AX to QX, and
  • for at least one argument, the conversion from AX to PX is better than the conversion from AX to QX.

When performing this evaluation, if MP or MQ is applicable in its expanded form, then PX or QX refers to a parameter in the expanded form of the parameter list.


7.4.2.3 Better conversion

Given an implicit conversion C1 that converts from a type S to a type T1, and an implicit conversion C2 that converts from a type S to a type T2, the better conversion of the two conversions is determined as follows:

  • If T1 and T2 are the same type, neither conversion is better.
  • If S is T1, C1 is the better conversion.
  • If S is T2, C2 is the better conversion.
  • If an implicit conversion from T1 to T2 exists, and no implicit conversion from T2 to T1 exists, C1 is the better conversion.
  • If an implicit conversion from T2 to T1 exists, and no implicit conversion from T1 to T2 exists, C2 is the better conversion.
  • If T1 is sbyte and T2 is byte, ushort, uint, or ulong, C1 is the better conversion.
  • If T2 is sbyte and T1 is byte, ushort, uint, or ulong, C2 is the better conversion.
  • If T1 is short and T2 is ushort, uint, or ulong, C1 is the better conversion.
  • If T2 is short and T1 is ushort, uint, or ulong, C2 is the better conversion.
  • If T1 is int and T2 is uint, or ulong, C1 is the better conversion.
  • If T2 is int and T1 is uint, or ulong, C2 is the better conversion.
  • If T1 is long and T2 is ulong, C1 is the better conversion.
  • If T2 is long and T1 is ulong, C2 is the better conversion.
  • Otherwise, neither conversion is better.

If an implicit conversion C1 is defined by these rules to be a better conversion than an implicit conversion C2, then it is also the case that C2 is a worse conversion than C1.


7.4.3 Function member invocation

This section describes the process that takes place at run-time to invoke a particular function member. It is assumed that a compile-time process has already determined the particular member to invoke, possibly by applying overload resolution to a set of candidate function members.

For purposes of describing the invocation process, function members are divided into two categories:

  • Static function members. These are static methods, constructors, static property accessors, and user-defined operators. Static function members are always non-virtual.
  • Instance function members. These are instance methods, instance property accessors, and indexer accessors. Instance function members are either non-virtual or virtual, and are always invoked on a particular instance. The instance is computed by an instance expression, and it becomes accessible within the function member as this (§7.5.7).

The run-time processing of a function member invocation consists of the following steps, where M is the function member and, if M is an instance member, E is the instance expression:

  • If M is a static function member:
    • The argument list is evaluated as described in §7.4.1.
    • M is invoked.
  • If M is an instance function member declared in a value-type:
    • E is evaluated. If this evaluation causes an exception, then no further steps are executed.
    • If E is not classified as a variable, then a temporary local variable of E’s type is created and the value of E is assigned to that variable. E is then reclassified as a reference to that temporary local variable. The temporary variable is accessible as this within M, but not in any other way. Thus, only when E is a true variable is it possible for the caller to observe the changes that M makes to this.
    • The argument list is evaluated as described in §7.4.1.
    • M is invoked. The variable referenced by E becomes the variable referenced by this.
  • If M is an instance function member declared in a reference-type:
    • E is evaluated. If this evaluation causes an exception, then no further steps are executed.
    • The argument list is evaluated as described in §7.4.1.
    • If the type of E is a value-type, a boxing conversion (§4.3.1) is performed to convert E to type object, and E is considered to be of type object in the following steps.
    • The value of E is checked to be valid. If the value of E is null, a NullReferenceException is thrown and no further steps are executed.
    • The function member implementation to invoke is determined: If M is a non-virtual function member, then M is the function member implementation to invoke. Otherwise, M is a virtual function member and the function member implementation to invoke is determined through virtual function member lookup (§7.4.4) or interface function member lookup (§7.4.5).
    • The function member implementation determined in the step above is invoked. The object referenced by E becomes the object referenced by this.


7.4.3.1 Invocations on boxed instances

A function member implemented in a value-type can be invoked through a boxed instance of that value-type in the following situations:

  • When the function member is an override of a method inherited from type object and is invoked through an instance expression of type object.
  • When the function member is an implementation of an interface function member and is invoked through an instance expression of an interface-type.
  • When the function member is invoked through a delegate.

In these situations, the boxed instance is considered to contain a variable of the value-type, and this variable becomes the variable referenced by this within the function member invocation. This in particular means that when a function member is invoked on a boxed instance, it is possible for the function member to modify the value contained in the boxed instance.


7.4.4 Virtual function member lookup

© 1987-2000 Microsoft Corporation. All rights reserved.


7.4.5 Interface function member lookup

© 1987-2000 Microsoft Corporation. All rights reserved.


7.5 Primary expressions

primary-expression:
literal
simple-name
parenthesized-expression
member-access
invocation-expression
element-access
this-access
base-access
post-increment-expression
post-decrement-expression
new-expression
typeof-expression
sizeof-expression
checked-expression
unchecked-expression


7.5.1 Literals

A primary-expression that consists of a literal (§2.5.3) is classified as a value.


7.5.2 Simple names

A simple-name consists of a single identifier.

simple-name:
identifier

A simple-name is evaluated and classified as follows:

  • If the simple-name appears within a block and if the block contains a local variable or parameter with the given name, then the simple-name refers to that local variable or parameter and is classified as a variable.
  • Otherwise, for each type T, starting with the immediately enclosing class, struct, or enumeration declaration and continuing with each enclosing outer class or struct declaration (if any), if a member lookup of the simple-name in T produces a match:
    • If T is the immediately enclosing class or struct type and the lookup identifies one or more methods, the result is a method group with an associated instance expression of this.
    • If T is the immediately enclosing class or struct type, if the lookup identifies an instance member, and if the reference occurs within the block of a constructor, an instance method, or an instance accessor, the result is exactly the same as a member access (§7.5.4) of the form this.E, where E is the simple-name.
    • Otherwise, the result is exactly the same as a member access (§7.5.4) of the form T.E, where E is the simple-name. In this case, it is an error for the simple-name to refer to an instance member.
  • Otherwise, starting with the namespace declaration in which the simple-name occurs (if any), continuing with each enclosing namespace declaration (if any), and ending with the global namespace, the following steps are evaluated until an entity is located:
    • If the namespace contains a namespace member with the given name, then the simple-name refers to that member and, depending on the member, is classified as a namespace or a type.
    • Otherwise, if the namespace declaration contains a using-alias-directive that associates the given name with an imported namespace or type, then the simple-name refers to that namespace or type.
    • Otherwise, if the namespaces imported by the using-namespace-directives of the namespace declaration contain exactly one type with the given name, then the simple-name refers to that type.
    • Otherwise, if the namespaces imported by the using-namespace-directives of the namespace declaration contain more than one type with the given name, then the simple-name is ambiguous and an error occurs.
  • Otherwise, the name given by the simple-name is undefined and an error occurs.


7.5.2.1 Invariant meaning in blocks

For each occurrence of a given identifier as a simple-name in an expression, every other occurrence of the same identifier as a simple-name in an expression within the immediately enclosing block (§8.2) or switch-block (§8.7.2) must refer to the same entity. This rule ensures that the meaning of an name in the context of an expression is always the same within a block.

The example

class Test
{
   double x;
   void F(bool b) {
      x = 1.0;
      if (b) {
         int x = 1;
      }
   }
}

is in error because x refers to different entities within the outer block (the extent of which includes the nested block in the if statement). In contrast, the example

class Test
{
   double x;
   void F(bool b) {
      if (b) {
         x = 1.0;
      }
      else {
         int x = 1;
      }
   }
}

is permitted because the name x is never used in the outer block.

Note that the rule of invariant meaning applies only to simple names. It is perfectly valid for the same identifier to have one meaning as a simple name and another meaning as right operand of a member access (§7.5.4). For example:

struct Point
{
   int x, y;
   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }
}

The example above illustrates a common pattern of using the names of fields as parameter names in a constructor. In the example, the simple names x and y refer to the parameters, but that does not prevent the member access expressions this.x and this.y from accessing the fields.


7.5.3 Parenthesized expressions

A parenthesized-expression consists of an expression enclosed in parentheses.

parenthesized-expression:
( expression )

A parenthesized-expression is evaluated by evaluating the expression within the parentheses. If the expression within the parentheses denotes a namespace, type, or method group, an error occurs. Otherwise, the result of the parenthesized-expression is the result of the evaluation of the contained expression.


7.5.4 Member access

A member-access consists of a primary-expression or a predefined-type, followed by a “.” token, followed by an identifier.

member-access:
primary-expression . identifier
predefined-type . identifier
predefined-type: one of
bool byte char decimal double float int long
object sbyte short string uint ulong ushort

A member-access of the form E.I, where E is a primary-expression or a predefined-type and I is an identifier, is evaluated and classified as follows:

  • If E is a namespace and I is the name of an accessible member of that namespace, then the result is that member and, depending on the member, is classified as a namespace or a type.
  • If E is a predefined-type or a primary-expression classified as a type, and a member lookup (§7.3) of I in E produces a match, then E.I is evaluated and classified as follows:
    • If I identifies a type, then the result is that type.
    • If I identifies one or more methods, then the result is a method group with no associated instance expression.
    • If I identifies a static property, then the result is a property access with no associated instance expression.
    • If I identifies a static field:
      • If the field is readonly and the reference occurs outside the static constructor of the class or struct in which the field is declared, then the result is a value, namely the value of the static field I in E.
      • Otherwise, the result is a variable, namely the static field I in E.
    • If I identifies a static event:
      • If the reference occurs within the class or struct in which the event is declared, then E.I is processed exactly as if I was a static field or property.
      • Otherwise, the result is an event access with no associated instance expression.
    • If I identifies a constant, then the result is a value, namely the value of that constant.
    • If I identifies an enumeration member, then the result is a value, namely the value of that enumeration member.
    • Otherwise, E.I is an invalid member reference, and an error occurs.
  • If E is a property access, indexer access, variable, or value, the type of which is T, and a member lookup (§7.3) of I in T produces a match, then E.I is evaluated and classified as follows:
    • First, if E is a property or indexer access, then the value of the property or indexer access is obtained (§7.1.1) and E is reclassified as a value.
    • If I identifies one or more methods, then the result is a method group with an associated instance expression of E.
    • If I identifies an instance property, then the result is a property access with an associated instance expression of E.
    • If T is a class-type and I identifies an instance field of that class-type:
      • If the value of E is null, then a NullReferenceException is thrown.
      • Otherwise, if the field is readonly and the reference occurs outside an instance constructor of the class in which the field is declared, then the result is a value, namely the value of the field I in the object referenced by E.
      • Otherwise, the result is a variable, namely the field I in the object referenced by E.
    • If T is a struct-type and I identifies an instance field of that struct-type:
      • If E is a value, or if the field is readonly and the reference occurs outside an instance constructor of the struct in which the field is declared, then the result is a value, namely the value of the field I in the struct instance given by E.
      • Otherwise, the result is a variable, namely the field I in the struct instance given by E.
    • If I identifies an instance event:
      • If the reference occurs within the class or struct in which the event is declared, then E.I is processed exactly as if I was an instance field or property.
      • Otherwise, the result is an event access with an associated instance expression of E.
  • Otherwise, E.I is an invalid member reference, and an error occurs.


7.5.4.1 Identical simple names and type names

In a member access of the form E.I, if E is a single identifier, and if the meaning of E as a simple-name (§7.5.2) is a constant, field, property, local variable, or parameter with the same type as the meaning of E as a type-name (§3.6), then both possible meanings of E are permitted. The two possible meanings of E.I are never ambiguous, since I must necessarily be a member of the type E in both cases. In other words, the rule simply permits access to the static members of E where an error would have otherwise occurred. For example:

struct Color
{
   public static readonly Color White = new Color(...);
   public static readonly Color Black = new Color(...);
   public Color Complement() {...}
}
class A
{
   public Color Color;               // Field Color of type Color
   void F() {
      Color = Color.Black;          // References Color.Black static member
      Color = Color.Complement();   // Invokes Complement() on Color field
   }
   static void G() {
      Color c = Color.White;         // References Color.White static member
   }
}

Within the A class, those occurrences of the Color identifier that reference the Color type are underlined, and those that reference the Color field are not underlined.


7.5.5 Invocation expressions

An invocation-expression is used to invoke a method.

invocation-expression:
primary-expression ( argument-listopt )

The primary-expression of an invocation-expression must be a method group or a value of a delegate-type. If the primary-expression is a method group, the invocation-expression is a method invocation (§7.5.5.1). If the primary-expression is a value of a delegate-type, the invocation-expression is a delegate invocation (§7.5.5.2). If the primary-expression is neither a method group nor a value of a delegate-type, an error occurs.

The optional argument-list (§7.4.1) provides values or variable references for the parameters of the method.

The result of evaluating an invocation-expression is classified as follows:

  • If the invocation-expression invokes a method or delegate that returns void, the result is nothing. An expression that is classified as nothing cannot be an operand of any operator, and is permitted only in the context of a statement-expression (§8.6).
  • Otherwise, the result is a value of the type returned by the method or delegate.


7.5.5.1 Method invocations

For a method invocation, the primary-expression of the invocation-expression must be a method group. The method group identifies the one method to invoke or the set of overloaded methods from which to choose a specific method to invoke. In the latter case, determination of the specific method to invoke is based on the context provided by the types of the arguments in the argument-list.

The compile-time processing of a method invocation of the form M(A), where M is a method group and A is an optional argument-list, consists of the following steps:

  • The set of candidate methods for the method invocation is constructed. Starting with the set of methods associated with M, which were found by a previous member lookup (§7.3), the set is reduced to those methods that are applicable with respect to the argument list A. The set reduction consists of applying the following rules to each method T.N in the set, where T is the type in which the method N is declared:
    • If N is not applicable with respect to A (§7.4.2.1), then N is removed from the set.
    • If N is applicable with respect to A (§7.4.2.1), then all methods declared in a base type of T are removed from the set.
  • If the resulting set of candidate methods is empty, then no applicable methods exist, and an error occurs. If the candidate methods are not all declared in the same type, the method invocation is ambiguous, and an error occurs (this latter situation can only occur for an invocation of a method in an interface that has multiple direct base interfaces, as described in §13.2.5).
  • The best method of the set of candidate methods is identified using the overload resolution rules of §7.4.2. If a single best method cannot be identified, the method invocation is ambiguous, and an error occurs.
  • Given a best method, the invocation of the method is validated in the context of the method group: If the best method is a static method, the method group must have resulted from a simple-name or a member-access through a type. If the best method is an instance method, the method group must have resulted from a simple-name, a member-access through a variable or value, or a base-access. If neither of these requirements are true, a compile-time error occurs.

Once a method has been selected and validated at compile-time by the above steps, the actual run-time invocation is processed according to the rules of function member invocation described in §7.4.3.

The intuitive effect of the resolution rules described above is as follows: To locate the particular method invoked by a method invocation, start with the type indicated by the method invocation and proceed up the inheritance chain until at least one applicable, accessible, non-override method declaration is found. Then perform overload resolution on the set of applicable, accessible, non-override methods declared in that type and invoke the method thus selected.


7.5.5.2 Delegate invocations

For a delegate invocation, the primary-expression of the invocation-expression must be a value of a delegate-type. Furthermore, considering the delegate-type to be a function member with the same parameter list as the delegate-type, the delegate-type must be applicable (§7.4.2.1) with respect to the argument-list of the invocation-expression.

The run-time processing of a delegate invocation of the form D(A), where D is a primary-expression of a delegate-type and A is an optional argument-list, consists of the following steps:

  • D is evaluated. If this evaluation causes an exception, no further steps are executed.
  • The value of D is checked to be valid. If the value of D is null, a NullReferenceException is thrown and no further steps are executed.
  • Otherwise, D is reference to a delegate instance. A function member invocation (§7.4.3) is performed on the method referenced by the delegate. If the method is an instance method, the instance of the invocation becomes the instance referenced by the delegate.


7.5.6 Element access

An element-access consists of a primary-expression, followed by a “[“ token, followed by an expression-list, followed by a “]” token. The expression-list consists of one or more expressions, separated by commas.

element-access:
primary-expression [ expression-list ]
expression-list:
expression
expression-list , expression

If the primary-expression of an element-access is a value of an array-type, the element-access is an array access (§7.5.6.1). Otherwise, the primary-expression must be a variable or value of a class, struct, or interface type that has one or more indexer members, and the element-access is then an indexer access (§7.5.6.2).


7.5.6.1 Array access

For an array access, the primary-expression of the element-access must be a value of an array-type. The number of expressions in the expression-list must be the same as the rank of the array-type, and each expression must be of type int, uint, long, ulong, or of a type that can be implicitly converted to one or more of these types.

The result of evaluating an array access is a variable of the element type of the array, namely the array element selected by the value(s) of the expression(s) in the expression-list.

The run-time processing of an array access of the form P[A], where P is a primary-expression of an array-type and A is an expression-list, consists of the following steps:

  • P is evaluated. If this evaluation causes an exception, no further steps are executed.
  • The index expressions of the expression-list are evaluated in order, from left to right. Following evaluation of each index expression, an implicit conversion (§6.1) to one of the following types is performed: int, uint, long, ulong. The first type in this list for which an implicit conversion exists is chosen. For instance, if the index expression is of type short then an implicit conversion to int is performed, since implicit conversions from short to int and from short to long are possible. If evaluation of an index expression or the subsequent implicit conversion causes an exception, then no further index expressions are evaluated and no further steps are executed.
  • The value of P is checked to be valid. If the value of P is null, a NullReferenceException is thrown and no further steps are executed.
  • The value of each expression in the expression-list is checked against the actual bounds of each dimension of the array instance referenced by P. If one or more values are out of range, an IndexOutOfRangeException is thrown and no further steps are executed.
  • The location of the array element given by the index expression(s) is computed, and this location becomes the result of the array access.


7.5.6.2 Indexer access

For an indexer access, the primary-expression of the element-access must be a variable or value of a class, struct, or interface type, and this type must implement one or more indexers that are applicable with respect to the expression-list of the element-access.

The compile-time processing of an indexer access of the form P[A], where P is a primary-expression of a class, struct, or interface type T, and A is an expression-list, consists of the following steps:

  • The set of indexers provided by T is constructed. The set consists of all indexers declared in T or a base type of T that are not override declarations and are accessible in the current context (§3.3).
  • The set is reduced to those indexers that are applicable and not hidden by other indexers. The following rules are applied to each indexer S.I in the set, where S is the type in which the indexer I is declared:
    • If I is not applicable with respect to A (§7.4.2.1), then I is removed from the set.
    • If I is applicable with respect to A (§7.4.2.1), then all indexers declared in a base type of S are removed from the set.
  • If the resulting set of candidate indexers is empty, then no applicable indexers exist, and an error occurs. If the candidate indexers are not all declared in the same type, the indexer access is ambiguous, and an error occurs (this latter situation can only occur for an indexer access on an instance of an interface that has multiple direct base interfaces).
  • The best indexer of the set of candidate indexers is identified using the overload resolution rules of §7.4.2. If a single best indexer cannot be identified, the indexer access is ambiguous, and an error occurs.
  • The result of processing the indexer access is an expression classified as an indexer access. The indexer access expression references the indexer determined in the step above, and has an associated instance expression of P and an associated argument list of A.

Depending on the context in which it is used, an indexer access causes invocation of either the get-accessor or the set-accessor of the indexer. If the indexer access is the target of an assignment, the set-accessor is invoked to assign a new value (§7.13.1). In all other cases, the get-accessor is invoked to obtain the current value (§7.1.1).


7.5.6.3 String indexing

The string class implements an indexer that allows the individual characters of a string to be accessed. The indexer of the string class has the following declaration:

public char this[int index] { get; }

In other words, a read-only indexer that takes a single argument of type int and returns an element of type char. Values passed for the index argument must be greater than or equal to zero and less than the length of the string.


7.5.7 This access

A this-access consists of the reserved word this.

this-access:
this

A this-access is permitted only in the block of a constructor, an instance method, or an instance accessor. It has one of the following meanings:

  • When this is used in a primary-expression within a constructor of a class, it is classified as a value. The type of the value is the class within which the reference occurs, and the value is a reference to the object being constructed.
  • When this is used in a primary-expression within an instance method or instance accessor of a class, it is classified as a value. The type of the value is the class within which the reference occurs, and the value is a reference to the object for which the method or accessor was invoked.
  • When this is used in a primary-expression within a constructor of a struct, it is classified as a variable. The type of the variable is the struct within which the reference occurs, and the variable represents the struct being constructed. The this variable of a constructor of a struct behaves exactly the same as an out parameter of the struct type—this in particular means that the variable must be definitely assigned in every execution path of the constructor.
  • When this is used in a primary-expression within an instance method or instance accessor of a struct, it is classified as a variable. The type of the variable is the struct within which the reference occurs, and the variable represents the struct for which the method or accessor was invoked. The this variable of an instance method of a struct behaves exactly the same as a ref parameter of the struct type.

Use of this in a primary-expression in a context other than the ones listed above is an error. In particular, it is not possible to refer to this in a static method, a static property accessor, or in a variable-initializer of a field declaration.


7.5.8 Base access

A base-access consists of the reserved word base followed by either a “.” token and an identifier or an expression-list enclosed in square brackets:

base-access:
base . identifier
base [ expression-list ]

A base-access is used to access base class members that are hidden by similarly named members in the current class or struct. A base-access is permitted only in the block of a constructor, an instance method, or an instance accessor. When base.I occurs in a class or struct, I must denote a member of the base class of that class or struct. Likewise, when base[E] occurs in a class, an applicable indexer must exist in the base class.

At compile-time, base-access expressions of the form base.I and base[E] are evaluated exactly as if they were written ((B)this).I and ((B)this)[E], where B is the base class of the class or struct in which the construct occurs. Thus, base.I and base[E] correspond to this.I and this[E], except this is viewed as an instance of the base class.

When a base-access references a function member (a method, property, or indexer), the function member is considered non-virtual for purposes of function member invocation (§7.4.3). Thus, within an override of a virtual function member, a base-access can be used to invoke the inherited implementation of the function member. If the function member referenced by a base-access is abstract, an error occurs.


7.5.9 Postfix increment and decrement operators

post-increment-expression:
primary-expression ++
post-decrement-expression:
primary-expression --

The operand of a postfix increment or decrement operation must be an expression classified as a variable, a property access, or an indexer access. The result of the operation is a value of the same type as the operand.

If the operand of a postfix increment or decrement operation is a property or indexer access, the property or indexer must have both a get and a set accessor. If this is not the case, a compile-time error occurs.

Unary operator overload resolution (§7.2.3) is applied to select a specific operator implementation. Predefined ++ and -- operators exist for the following types: sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, and any enum type. The predefined ++ operators return the value produced by adding 1 to the operand, and the predefined -- operators return the value produced by subtracting 1 from the operand.

The run-time processing of a postfix increment or decrement operation of the form x++ or x-- consists of the following steps:

  • If x is classified as a variable:
    • x is evaluated to produce the variable.
    • The value of x is saved.
    • The selected operator is invoked with the saved value of x as its argument.
    • The value returned by the operator is stored in the location given by the evaluation of x.
    • The saved value of x becomes the result of the operation.
  • If x is classified as a property or indexer access:
    • The instance expression (if x is not static) and the argument list (if x is an indexer access) associated with x are evaluated, and the results are used in the subsequent get and set accessor invocations.
    • The get accessor of x is invoked and the returned value is saved.
    • The selected operator is invoked with the saved value of x as its argument.
    • The set accessor of x is invoked with the value returned by the operator as its value argument.
    • The saved value of x becomes the result of the operation.

The ++ and -- operators also support prefix notation, as described in §7.6.7. The result of x++ or x-- is the value of x before the operation, whereas the result of ++x or --x is the value of x after the operation. In either case, x itself has the same value after the operation.

An operator ++ or operator -- implementation can be invoked using either postfix or prefix notation. It is not possible to have separate operator implementations for the two notations.


7.5.10 new operator

The new operator is used to create new instances of types.

new-expression:
object-creation-expression
array-creation-expression
delegate-creation-expression

There are three forms of new expressions:

  • Object creation expressions are used to create a new instances of class types and value types.
  • Array creation expressions are used to create new instances of array types.
  • Delegate creation expressions are used to create new instances of delegate types.

The new operator implies creation of an instance of a type, but does not necessarily imply dynamic allocation of memory. In particular, instances of value types require no additional memory beyond the variables in which they reside, and no dynamic allocations occur when new is used to create instances of value types.


7.5.10.1 Object creation expressions

An object-creation-expression is used to create a new instance of a class-type or a value-type.

object-creation-expression:
new type ( argument-listopt )

The type of an object-creation-expression must be a class-type or a value-type. The type cannot be an abstract class-type.

The optional argument-list (§7.4.1) is permitted only if the type is a class-type or a struct-type.

The compile-time processing of an object-creation-expression of the form new T(A), where T is a class-type or a value-type and A is an optional argument-list, consists of the following steps:

  • If T is a value-type and A is not present:
    • The object-creation-expression is a default constructor invocation. The result of the object-creation-expression is a value of type T, namely the default value for T as defined in §4.1.1.
  • Otherwise, if T is a class-type or a struct-type:
    • If T is an abstract class-type, an error occurs.
    • The constructor to invoke is determined using the overload resolution rules of §7.4.2. The set of candidate constructors consists of all accessible constructors declared in T. If the set of candidate constructors is empty, or if a single best constructor cannot be identified, an error occurs.
    • The result of the object-creation-expression is a value of type T, namely the value produced by invoking the constructor determined in the step above.
  • Otherwise, the object-creation-expression is invalid, and an error occurs.

The run-time processing of an object-creation-expression of the form new T(A), where T is class-type or a struct-type and A is an optional argument-list, consists of the following steps:

  • If T is a class-type:
    • A new instance of class T is allocated. If there is not enough memory available to allocate the new instance, an OutOfMemoryException is thrown and no further steps are executed.
    • All fields of the new instance are initialized to their default values (§5.2).
    • The constructor is invoked according to the rules of function member invocation (§7.4.3). A reference to the newly allocated instance is automatically passed to the constructor and the instance can be accessed from within the constructor as this.
  • If T is a struct-type:
    • An instance of type T is created by allocating a temporary local variable. Since a constructor of a struct-type is required to definitely assign a value to each field of the instance being created, no initialization of the temporary variable is necessary.
    • The constructor is invoked according to the rules of function member invocation (§7.4.3). A reference to the newly allocated instance is automatically passed to the constructor and the instance can be accessed from within the constructor as this.


7.5.10.2 Array creation expressions

An array-creation-expression is used to create a new instance of an array-type.

array-creation-expression:
new non-array-type [ expression-list ] rank-specifiersopt array-initializeropt
new array-type array-initializer

An array creation expression of first form allocates an array instance of the type that results from deleting each of the individual expressions from the expression list. For example, the array creation expression new int[10, 20] produces an array instance of type int[,], and the array creation expression new int[10][,] produces an array of type int[][,]. Each expression in the expression list must be of type int, uint, long, or ulong, or of a type that can be implicitly converted to one or more of these types. The value of each expression determines the length of the corresponding dimension in the newly allocated array instance.

If an array creation expression of the first form includes an array initializer, each expression in the expression list must be a constant and the rank and dimension lengths specified by the expression list must match those of the array initializer.

In an array creation expression of the second form, the rank of the specified array type must match that of the array initializer. The individual dimension lengths are inferred from the number of elements in each of the corresponding nesting levels of the array initializer. Thus, the expression

new int[,] {{0, 1}, {2, 3}, {4, 5}};

exactly corresponds to

new int[3, 2] {{0, 1}, {2, 3}, {4, 5}};

Array initializers are further described in §12.6.

The result of evaluating an array creation expression is classified as a value, namely a reference to the newly allocated array instance. The run-time processing of an array creation expression consists of the following steps:

  • The dimension length expressions of the expression-list are evaluated in order, from left to right. Following evaluation of each expression, an implicit conversion (§6.1) to type int is performed. If evaluation of an expression or the subsequent implicit conversion causes an exception, then no further expressions are evaluated and no further steps are executed.
  • The computed values for the dimension lengths are validated. If one or more of the values are less than zero, an IndexOutOfRangeException is thrown and no further steps are executed.
  • An array instance with the given dimension lengths is allocated. If there is not enough memory available to allocate the new instance, an OutOfMemoryException is thrown and no further steps are executed.
  • All elements of the new array instance are initialized to their default values (§5.2).
  • If the array creation expression contains an array initializer, then each expression in the array initializer is evaluated and assigned to its corresponding array element. The evaluations and assignments are performed in the order the expressions are written in the array initializer—in other words, elements are initialized in increasing index order, with the rightmost dimension increasing first. If evaluation of a given expression or the subsequent assignment to the corresponding array element causes an exception, then no further elements are initialized (and the remaining elements will thus have their default values).

An array creation expression permits instantiation of an array with elements of an array type, but the elements of such an array must be manually initialized. For example, the statement

int[][] a = new int[100][];

creates a single-dimensional array with 100 elements of type int[]. The initial value of each element is null. It is not possible for the same array creation expression to also instantiate the sub-arrays, and the statement

int[][] a = new int[100][5];      // Error

is an error. Instantiation of the sub-arrays must instead be performed manually, as in

int[][] a = new int[100][];
for (int i = 0; i < 100; i++) a[i] = new int[5];

When an array of arrays has a “rectangular” shape, that is when the sub-arrays are all of the same length, it is more efficient to use a multi-dimensional array. In the example above, instantiation of the array of arrays creates 101 objects—one outer array and 100 sub-arrays. In contrast,

int[,] = new int[100, 5];

creates only a single object, a two-dimensional array, and accomplishes the allocation in a single statement.


7.5.10.3 Delegate creation expressions

A delegate-creation-expression is used to create a new instance of a delegate-type.

delegate-creation-expression:
new delegate-type ( expression )

The argument of a delegate creation expression must be a method group or a value of a delegate-type. If the argument is a method group, it identifies the method and, for an instance method, the object for which to create a delegate. If the argument is a value of a delegate-type, it identifies a delegate instance of which to create a copy.

The compile-time processing of a delegate-creation-expression of the form new D(E), where D is a delegate-type and E is an expression, consists of the following steps:

  • If E is a method group:
    • If the method group resulted from a base-access, an error occurs.
    • The set of methods identified by E must include exactly one method with precisely the same signature and return type as those of D, and this becomes the method to which the newly created delegate refers. If no matching method exists, or if more than one matching methods exists, an error occurs. If the selected method is an instance method, the instance expression associated with E determines the target object of the delegate.
    • As in a method invocation, the selected method must be compatible with the context of the method group: If the method is a static method, the method group must have resulted from a simple-name or a member-access through a type. If the method is an instance method, the method group must have resulted from a simple-name or a member-access through a variable or value. If the selected method does not match the context of the method group, an error occurs.
    • The result is a value of type D, namely a newly created delegate that refers to the selected method and target object.
  • Otherwise, if E is a value of a delegate-type:
    • The delegate-type of E must have the exact same signature and return type as D, or otherwise an error occurs.
    • The result is a value of type D, namely a newly created delegate that refers to the same method and target object as E.
  • Otherwise, the delegate creation expression is invalid, and an error occurs.

The run-time processing of a delegate-creation-expression of the form new D(E), where D is a delegate-type and E is an expression, consists of the following steps:

  • If E is a method group:
    • If the method selected at compile-time is a static method, the target object of the delegate is null. Otherwise, the selected method is an instance method, and the target object of the delegate is determined from the instance expression associated with E:
      • The instance expression is evaluated. If this evaluation causes an exception, no further steps are executed.
      • If the instance expression is of a reference-type, the value computed by the instance expression becomes the target object. If the target object is null, a NullReferenceException is thrown and no further steps are executed.
      • If the instance expression is of a value-type, a boxing operation (§4.3.1) is performed to convert the value to an object, and this object becomes the target object.
    • A new instance of the delegate type D is allocated. If there is not enough memory available to allocate the new instance, an OutOfMemoryException is thrown and no further steps are executed.
    • The new delegate instance is initialized with a reference to the method that was determined at compile-time and a reference to the target object computed above.
  • If E is a value of a delegate-type:
    • E is evaluated. If this evaluation causes an exception, no further steps are executed.
    • If the value of E is null, a NullReferenceException is thrown and no further steps are executed.
    • A new instance of the delegate type D is allocated. If there is not enough memory available to allocate the new instance, an OutOfMemoryException is thrown and no further steps are executed.
    • The new delegate instance is initialized with references to the same method and object as the delegate instance given by E.

The method and object to which a delegate refers are determined when the delegate is instantiated and then remain constant for the entire lifetime of the delegate. In other words, it is not possible to change the target method or object of a delegate once it has been created.

It is not possible to create a delegate that refers to a constructor, property, indexer, or user-defined operator.

As described above, when a delegate is created from a method group, the signature and return type of the delegate determine which of the overloaded methods to select. In the example

delegate double DoubleFunc(double x);
class A
{
   DoubleFunc f = new DoubleFunc(Square);
   static float Square(float x) {
      return x * x;
   }
   static double Square(double x) {
      return x * x;
   }
}

the A.f field is initialized with a delegate that refers to the second Square method because that method exactly matches the signature and return type of DoubleFunc. Had the second Square method not been present, a compile-time error would have occurred.


7.5.11 typeof operator

The typeof operator is used to obtain the System.Type object for a type.

typeof-expression:
typeof ( type )

The result of a typeof-expression is the System.Type object for the indicated type.

The example

class Test
{
   static void Main() {
      Type[] t = {
         typeof(int),
         typeof(System.Int32),
         typeof(string),
         typeof(double[])
      };
      for (int i = 0; i < t.Length; i++) {
         Console.WriteLine(t[i].Name);
      }
   }
}

produces the following output:

Int32
Int32
String
Double[]

Note that int and System.Int32 are the same type.


7.5.12 sizeof operator

C# Language Reference

7.5.12 checked and unchecked operators

[This topic is part of a preview and is subject to change in future releases. Blank topics are included as placeholders.]

The checked and unchecked operators are used to control the overflow checking context for integral-type arithmetic operations and conversions.

checked-expression:
checked ( expression )
unchecked-expression:
unchecked ( expression )

The checked operator evaluates the contained expression in a checked context, and the unchecked operator evaluates the contained expression in an unchecked context. A checked-expression or unchecked-expression corresponds exactly to a parenthesized-expression (§7.5.3), except that the contained expression is evaluated in the given overflow checking context.

The overflow checking context can also be controlled through the checked and unchecked statements (§8.11).

The following operations are affected by the overflow checking context established by the checked and unchecked operators and statements:

  • The predefined ++ and -- unary operators (§7.5.9 and §7.6.7), when the operand is of an integral type.
  • The predefined - unary operator (§7.6.2), when the operand is of an integral type.
  • The predefined +, -, *, and / binary operators (§7.7), when both operands are of integral types.
  • Explicit numeric conversions (§6.2.1) from one integral type to another integral type.

When one of the above operations produce a result that is too large to represent in the destination type, the context in which the operation is performed controls the resulting behavior:

  • In a checked context, if the operation is a constant expression (§7.15), a compile-time error occurs. Otherwise, when the operation is performed at run-time, an OverflowException is thrown.
  • In an unchecked context, the result is truncated by discarding any high-order bits that do not fit in the destination type.

When a non-constant expression (an expression that is evaluated at run-time) is not enclosed by any checked or unchecked operators or statements, the effect of an overflow during the run-time evaluation of the expression depends on external factors (such as compiler switches and execution environment configuration). The effect is however guaranteed to be either that of a checked evaluation or that of an unchecked evaluation.

For constant expressions (expressions that can be fully evaluated at compile-time), the default overflow checking context is always checked. Unless a constant expression is explicitly placed in an unchecked context, overflows that occur during the compile-time evaluation of the expression always cause compile-time errors.

In the example

class Test
{
   static int x = 1000000;
   static int y = 1000000;
   static int F() {
      return checked(x * y);      // Throws OverflowException
   }
   static int G() {
      return unchecked(x * y);   // Returns -727379968
   }
   static int H() {
      return x * y;               // Depends on default
   }
}

no compile-time errors are reported since neither of the expressions can be evaluated at compile-time. At run-time, the F() method throws an OverflowException, and the G() method returns –727379968 (the lower 32 bits of the out-of-range result). The behavior of the H() method depends on the default overflow checking context for the compilation, but it is either the same as F() or the same as G().

In the example

class Test
{
   const int x = 1000000;
   const int y = 1000000;
   static int F() {
      return checked(x * y);      // Compile error, overflow
   }
   static int G() {
      return unchecked(x * y);   // Returns -727379968
   }
   static int H() {
      return x * y;               // Compile error, overflow
   }
}

the overflows that occur when evaluating the constant expressions in F() and H() cause compile-time errors to be reported because the expressions are evaluated in a checked context. An overflow also occurs when evaluating the constant expression in G(), but since the evaluation takes place in an unchecked context, the overflow is not reported.

The checked and unchecked operators only affect the overflow checking context for those operations that are textually contained within the “(” and “)” tokens. The operators have no effect on function members that are invoked as a result of evaluating the contained expression. In the example

class Test
{
   static int Multiply(int x, int y) {
      return x * y;
   }
   static int F() {
      return checked(Multiply(1000000, 1000000));
   }
}

the use of checked in F() does not affect the evaluation of x * y in Multiply(), and x * y is therefore evaluated in the default overflow checking context.

The unchecked operator is convenient when writing constants of the signed integral types in hexadecimal notation. For example:

class Test
{
   public const int AllBits = unchecked((int)0xFFFFFFFF);
   public const int HighBit = unchecked((int)0x80000000);
}

Both of the hexadecimal constants above are of type uint. Because the constants are outside the int range, without the unchecked operator, the casts to int would produce compile-time errors.


7.6 Unary expressions

unary-expression:
primary-expression
+ unary-expression
- unary-expression
! unary-expression
~ unary-expression
* unary-expression
& unary-expression
pre-increment-expression
pre-decrement-expression
cast-expression


7.6.1 Unary plus operator

For an operation of the form +x, unary operator overload resolution (§7.2.3) is applied to select a specific operator implementation. The operand is converted to the parameter type of the selected operator, and the type of the result is the return type of the operator. The predefined unary plus operators are:

int operator +(int x);
uint operator +(uint x);
long operator +(long x);
ulong operator +(ulong x);
float operator +(float x);
double operator +(double x);
decimal operator +(decimal x);

For each of these operators, the result is simply the value of the operand.


7.6.2 Unary minus operator

For an operation of the form –x, unary operator overload resolution (§7.2.3) is applied to select a specific operator implementation. The operand is converted to the parameter type of the selected operator, and the type of the result is the return type of the operator. The predefined negation operators are:

  • Integer negation:
int operator –(int x);
long operator –(long x);

The result is computed by subtracting x from zero. In a checked context, if the value of x is the maximum negative int or long, an OverflowException is thrown. In an unchecked context, if the value of x is the maximum negative int or long, the result is that same value and the overflow is not reported.

If the operand of the negation operator is of type uint, it is converted to type long, and the type of the result is long. An exception is the rule that permits the int value -2147483648 (-231) to be written as a decimal integer literal (§2.5.3.2).

If the operand of the negation operator is of type ulong, an error occurs. An exception is the rule that permits the long value -9223372036854775808 (-263) to be written as decimal integer literal (§2.5.3.2).

  • Floating-point negation:
float operator –(float x);
double operator –(double x);

The result is the value of x with its sign inverted. If x is NaN, the result is also NaN.

  • Decimal negation:
decimal operator –(decimal x);

The result is computed by subtracting x from zero.


7.6.3 Logical negation operator

For an operation of the form !x, unary operator overload resolution (§7.2.3) is applied to select a specific operator implementation. The operand is converted to the parameter type of the selected operator, and the type of the result is the return type of the operator. Only one predefined logical negation operator exists:

bool operator !(bool x);

This operator computes the logical negation of the operand: If the operand is true, the result is false. If the operand is false, the result is true.


7.6.4 Bitwise complement operator

For an operation of the form ~x, unary operator overload resolution (§7.2.3) is applied to select a specific operator implementation. The operand is converted to the parameter type of the selected operator, and the type of the result is the return type of the operator. The predefined bitwise complement operators are:

int operator ~(int x);
uint operator ~(uint x);
long operator ~(long x);
ulong operator ~(ulong x);

For each of these operators, the result of the operation is the bitwise complement of x.

Every enumeration type E implicitly provides the following bitwise complement operator:

E operator ~(E x);

The result of evaluating ~x, where x is an expression of an enumeration type E with an underlying type U, is exactly the same as evaluating (E)(~(U)x).


7.6.5 Indirection operator

© 1987-2000 Microsoft Corporation. All rights reserved.


7.6.6 Address operator

© 1987-2000 Microsoft Corporation. All rights reserved.


7.6.7 Prefix increment and decrement operators

pre-increment-expression:
++ unary-expression
pre-decrement-expression:
-- unary-expression

The operand of a prefix increment or decrement operation must be an expression classified as a variable, a property access, or an indexer access. The result of the operation is a value of the same type as the operand.

If the operand of a prefix increment or decrement operation is a property or indexer access, the property or indexer must have both a get and a set accessor. If this is not the case, a compile-time error occurs.

Unary operator overload resolution (§7.2.3) is applied to select a specific operator implementation. Predefined ++ and -- operators exist for the following types: sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, and any enum type. The predefined ++ operators return the value produced by adding 1 to the operand, and the predefined -- operators return the value produced by subtracting 1 from the operand.

The run-time processing of a prefix increment or decrement operation of the form ++x or --x consists of the following steps:

  • If x is classified as a variable:
    • x is evaluated to produce the variable.
    • The selected operator is invoked with the value of x as its argument.
    • The value returned by the operator is stored in the location given by the evaluation of x.
    • The value returned by the operator becomes the result of the operation.
  • If x is classified as a property or indexer access:
    • The instance expression (if x is not static) and the argument list (if x is an indexer access) associated with x are evaluated, and the results are used in the subsequent get and set accessor invocations.
    • The get accessor of x is invoked.
    • The selected operator is invoked with the value returned by the get accessor as its argument.
    • The set accessor of x is invoked with the value returned by the operator as its value argument.
    • The value returned by the operator becomes the result of the operation.

The ++ and -- operators also support postfix notation, as described in §7.5.9. The result of x++ or x-- is the value of x before the operation, whereas the result of ++x or --x is the value of x after the operation. In either case, x itself has the same value after the operation.

An operator ++ or operator -- implementation can be invoked using either postfix or prefix notation. It is not possible to have separate operator implementations for the two notations.


7.6.8 Cast expressions

A cast-expression is used to explicitly convert an expression to a given type.

cast-expression:
( type ) unary-expression

A cast-expression of the form (T)E, where T is a type and E is a unary-expression, performs an explicit conversion (§6.2) of the value of E to type T. If no explicit conversion exists from the type of E to T, an error occurs. Otherwise, the result is the value produced by the explicit conversion. The result is always classified as a value, even if E denotes a variable.

The grammar for a cast-expression leads to certain syntactic ambiguities. For example, the expression (x)–y could either be interpreted as a cast-expression (a cast of –y to type x) or as an additive-expression combined with a parenthesized-expression (which computes the value x – y).

To resolve cast-expression ambiguities, the following rule exists: A sequence of one or more tokens (§2.4.6) enclosed in parentheses is considered the start of a cast-expression only if at least one of the following are true:

  • The sequence of tokens is correct grammar for a type, but not for an expression.
  • The sequence of tokens is correct grammar for a type, and the token immediately following the closing parentheses is the token “~”, the token “!”, the token “(”, an identifier (§2.5.1), a literal (§2.5.3), or any keyword (§2.5.2) except is.

The above rules mean that only if the construct is unambiguously a cast-expression is it considered a cast-expression.

The term “correct grammar” above means only that the sequence of tokens must conform to the particular grammatical production. It specifically does not consider the actual meaning of any constituent identifiers. For example, if x and y are identifiers, then x.y is correct grammar for a type, even if x.y doesn’t actually denote a type.

From the disambiguation rules it follows that, if x and y are identifiers, (x)y, (x)(y), and (x)(-y) are cast-expressions, but (x)-y is not, even if x identifies a type. However, if x is a keyword that identifies a predefined type (such as int), then all four forms are cast-expressions (because such a keyword could not possibly be an expression by itself).


7.7 Arithmetic operators

The *, /, %, +, and – operators are called the arithmetic operators.

multiplicative-expression:
unary-expression
multiplicative-expression * unary-expression
multiplicative-expression / unary-expression
multiplicative-expression % unary-expression
additive-expression:
multiplicative-expression
additive-expression + multiplicative-expression
additive-expression – multiplicative-expression


7.7.1 Multiplication operator

For an operation of the form x * y, binary operator overload resolution (§7.2.4) is applied to select a specific operator implementation. The operands are converted to the parameter types of the selected operator, and the type of the result is the return type of the operator.

The predefined multiplication operators are listed below. The operators all compute the product of x and y.

  • Integer multiplication:
int operator *(int x, int y);
uint operator *(uint x, uint y);
long operator *(long x, long y);
ulong operator *(ulong x, ulong y);

In a checked context, if the product is outside the range of the result type, an OverflowException is thrown. In an unchecked context, overflows are not reported and any significant high-order bits of the result are discarded.

  • Floating-point multiplication:
float operator *(float x, float y);
double operator *(double x, double y);

The product is computed according to the rules of IEEE 754 arithmetic. The following table lists the results of all possible combinations of nonzero finite values, zeros, infinities, and NaN’s. In the table, x and y are positive finite values. z is the result of x * y. If the result is too large for the destination type, z is infinity. If the result is too small for the destination type, z is zero.

 | +y | –y | +0 | –0 | +8 | –8 | NaN
+x | z | –z | +0 | –0 | +8 | –8 | NaN
–x | –z | z | –0 | +0 | –8 | +8 | NaN
+0 | +0 | –0 | +0 | –0 | NaN | NaN | NaN
–0 | –0 | +0 | –0 | +0 | NaN | NaN | NaN
+8 | +8 | –8 | NaN | NaN | +8 | –8 | NaN
–8 | –8 | +8 | NaN | NaN | –8 | +8 | NaN
NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN
  • Decimal multiplication:
decimal operator *(decimal x, decimal y);

If the resulting value is too large to represent in the decimal format, an OverflowException is thrown. If the result value is too small to represent in the decimal format, the result is zero.


7.7.2 Division operator

For an operation of the form x / y, binary operator overload resolution (§7.2.4) is applied to select a specific operator implementation. The operands are converted to the parameter types of the selected operator, and the type of the result is the return type of the operator.

The predefined division operators are listed below. The operators all compute the quotient of x and y.

  • Integer division:
int operator /(int x, int y);
uint operator /(uint x, uint y);
long operator /(long x, long y);
ulong operator /(ulong x, ulong y);

If the value of the right operand is zero, a DivideByZeroException is thrown.

The division rounds the result towards zero, and the absolute value of the result is the largest possible integer that is less than the absolute value of the quotient of the two operands. The result is zero or positive when the two operands have the same sign and zero or negative when the two operands have opposite signs.

If the left operand is the maximum negative int or long and the right operand is –1, an overflow occurs. In a checked context, this causes an OverflowException to be thrown. In an unchecked context, the overflow is not reported and the result is instead the value of the left operand.

  • Floating-point division:
float operator /(float x, float y);
double operator /(double x, double y);

The quotient is computed according to the rules of IEEE 754 arithmetic. The following table lists the results of all possible combinations of nonzero finite values, zeros, infinities, and NaN’s. In the table, x and y are positive finite values. z is the result of x / y. If the result is too large for the destination type, z is infinity. If the result is too small for the destination type, z is zero.

 | +y | –y | +0 | –0 | +8 | –8 | NaN
+x | z | –z | +8 | –8 | +0 | –0 | NaN
–x | –z | z | –8 | +8 | –0 | +0 | NaN
+0 | +0 | –0 | NaN | NaN | +0 | –0 | NaN
–0 | –0 | +0 | NaN | NaN | –0 | +0 | NaN
+8 | +8 | –8 | +8 | –8 | NaN | NaN | NaN
–8 | –8 | +8 | –8 | +8 | NaN | NaN | NaN
NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN
  • Decimal division:
decimal operator /(decimal x, decimal y);

If the value of the right operand is zero, a DivideByZeroException is thrown. If the resulting value is too large to represent in the decimal format, an OverflowException is thrown. If the result value is too small to represent in the decimal format, the result is zero.


7.7.3 Remainder operator

For an operation of the form x % y, binary operator overload resolution (§7.2.4) is applied to select a specific operator implementation. The operands are converted to the parameter types of the selected operator, and the type of the result is the return type of the operator.

The predefined remainder operators are listed below. The operators all compute the remainder of the division between x and y.

  • Integer remainder:
int operator %(int x, int y);
uint operator %(uint x, uint y);
long operator %(long x, long y);
ulong operator %(ulong x, ulong y);

The result of x % y is the value produced by x – (x / y) * y. If y is zero, a DivideByZeroException is thrown. The remainder operator never causes an overflow.

  • Floating-point remainder:
float operator %(float x, float y);
double operator %(double x, double y);

The following table lists the results of all possible combinations of nonzero finite values, zeros, infinities, and NaN’s. In the table, x and y are positive finite values. z is the result of x % y and is computed as x – n * y, where n is the largest possible integer that is less than or equal to x / y. This method of computing the remainder is analogous to that used for integer operands, but differs from the IEEE 754 definition (in which n is the integer closest to x / y).

 | +y | –y | +0 | –0 | +8 | –8 | NaN
+x | z | z | NaN | NaN | x | x | NaN
–x | –z | –z | NaN | NaN | –x | –x | NaN
+0 | +0 | +0 | NaN | NaN | +0 | +0 | NaN
–0 | –0 | –0 | NaN | NaN | –0 | –0 | NaN
+8 | NaN | NaN | NaN | NaN | NaN | NaN | NaN
–8 | NaN | NaN | NaN | NaN | NaN | NaN | NaN
NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN
  • Decimal remainder:
decimal operator %(decimal x, decimal y);

If the value of the right operand is zero, a DivideByZeroException is thrown. If the resulting value is too large to represent in the decimal format, an OverflowException is thrown. If the result value is too small to represent in the decimal format, the result is zero.


7.7.4 Addition operator

For an operation of the form x + y, binary operator overload resolution (§7.2.4) is applied to select a specific operator implementation. The operands are converted to the parameter types of the selected operator, and the type of the result is the return type of the operator.

The predefined addition operators are listed below. For numeric and enumeration types, the predefined addition operators compute the sum of the two operands. When one or both operands are of type string, the predefined addition operators concatenate the string representation of the operands.

  • Integer addition:
int operator +(int x, int y);
uint operator +(uint x, uint y);
long operator +(long x, long y);
ulong operator +(ulong x, ulong y);

In a checked context, if the sum is outside the range of the result type, an OverflowException is thrown. In an unchecked context, overflows are not reported and any significant high-order bits of the result are discarded.

  • Floating-point addition:
float operator +(float x, float y);
double operator +(double x, double y);

The sum is computed according to the rules of IEEE 754 arithmetic. The following table lists the results of all possible combinations of nonzero finite values, zeros, infinities, and NaN’s. In the table, x and y are nonzero finite values, and z is the result of x + y. If x and y have the same magnitude but opposite signs, z is positive zero. If x + y is too large to represent in the destination type, z is an infinity with the same sign as x + y. If x + y is too small to represent in the destination type, z is a zero with the same sign as x + y.

 | y | +0 | –0 | +8 | –8 | NaN
x | z | x | x | +8 | –8 | NaN
+0 | y | +0 | +0 | +8 | –8 | NaN
–0 | y | +0 | –0 | +8 | –8 | NaN
+8 | +8 | +8 | +8 | +8 | NaN | NaN
–8 | –8 | –8 | –8 | NaN | –8 | NaN
NaN | NaN | NaN | NaN | NaN | NaN | NaN
  • Decimal addition:
decimal operator +(decimal x, decimal y);

If the resulting value is too large to represent in the decimal format, an OverflowException is thrown. If the result value is too small to represent in the decimal format, the result is zero.

  • Enumeration addition. Every enumeration type implicitly provides the following predefined operators, where E is the enum type, and U is the underlying type of E:
E operator +(E x, U y);
E operator +(U x, E y);

The operators are evaluated exactly as (E)((U)x + (U)y).

  • String concatenation:
string operator +(string x, string y);
string operator +(string x, object y);
string operator +(object x, string y);

The binary + operator performs string concatenation when one or both operands are of type string. If an operand of string concatenation is null, an empty string is substituted. Otherwise, any non-string argument is converted to its string representation by invoking the virtual ToString() method inherited from type object. If ToString() returns null, an empty string is substituted.

The result of the string concatenation operator is a string that consists of the characters of the left operand followed by the characters of the right operand. The string concatenation operator never returns a null value. An OutOfMemoryException may be thrown if there is not enough memory available to allocate the resulting string.

  • Delegate combination. Every delegate type implicitly provides the following predefined operator, where D is the delegate type:
D operator +(D x, D y);

The binary + operator performs delegate combination when one or both operands are of a delegate type D. If the first operand is null, then the result of the operation is the value of the second operand. Otherwise, if the second operand is null, then the result of the operation is the value of the first operand. Otherwise, if D is a combinable delegate type (§15.1.1) then the result of the operation is a new delegate instance that, when invoked, invokes the first operand and then invokes the second operand. Otherwise, D is not a combinable delegate type, and a MulticastNotSupportedException is thrown.


7.7.5 Subtraction operator

For an operation of the form x – y, binary operator overload resolution (§7.2.4) is applied to select a specific operator implementation. The operands are converted to the parameter types of the selected operator, and the type of the result is the return type of the operator.

The predefined subtraction operators are listed below. The operators all subtract y from x.

  • Integer subtraction:
int operator –(int x, int y);
uint operator –(uint x, uint y);
long operator –(long x, long y);
ulong operator –(ulong x, ulong y);

In a checked context, if the difference is outside the range of the result type, an OverflowException is thrown. In an unchecked context, overflows are not reported and any significant high-order bits of the result are discarded.

  • Floating-point subtraction:
float operator –(float x, float y);
double operator –(double x, double y);

The difference is computed according to the rules of IEEE 754 arithmetic. The following table lists the results of all possible combinations of nonzero finite values, zeros, infinities, and NaN’s. In the table, x and y are nonzero finite values, and z is the result of x – y. If x and y are equal, z is positive zero. If x – y is too large to represent in the destination type, z is an infinity with the same sign as x – y. If x – y is too small to represent in the destination type, z is a zero with the same sign as x – y.

 | y | +0 | –0 | +8 | –8 | NaN
x | z | x | x | –8 | +8 | NaN
+0 | –y | +0 | +0 | –8 | +8 | NaN
–0 | –y | –0 | +0 | –8 | +8 | NaN
+8 | +8 | +8 | +8 | NaN | +8 | NaN
–8 | –8 | –8 | –8 | –8 | NaN | NaN
NaN | NaN | NaN | NaN | NaN | NaN | NaN
  • Decimal subtraction:
decimal operator –(decimal x, decimal y);

If the resulting value is too large to represent in the decimal format, an OverflowException is thrown. If the result value is too small to represent in the decimal format, the result is zero.

  • Enumeration subtraction. Every enumeration type implicitly provides the following predefined operator, where E is the enum type, and U is the underlying type of E:
U operator –(E x, E y);

This operator is evaluated exactly as (U)((U)x – (U)y). In other words, the operator computes the difference between the ordinal values of x and y, and the type of the result is the underlying type of the enumeration.

E operator –(E x, U y);

This operator is evaluated exactly as (E)((U)x – y). In other words, the operator subtracts a value from the underlying type of the enumeration, yielding a value of the enumeration.

  • Delegate removal. Every delegate type implicitly provides the following predefined operator, where D is the delegate type:
D operator –(D x, D y);

The binary - operator performs delegate removal when one or both operands are of a delegate type D.


7.8 Shift operators

The << and >> operators are used to perform bit shifting operations.

shift-expression:
additive-expression
shift-expression << additive-expression
shift-expression >> additive-expression

For an operation of the form x << count or x >> count, binary operator overload resolution (§7.2.4) is applied to select a specific operator implementation. The operands are converted to the parameter types of the selected operator, and the type of the result is the return type of the operator.

When declaring an overloaded shift operator, the type of the first operand must always be the class or struct containing the operator declaration, and the type of the second operand must always be int.

The predefined shift operators are listed below.

  • Shift left:
int operator <<(int x, int count);
uint operator <<(uint x, int count);
long operator <<(long x, int count);
ulong operator <<(ulong x, int count);

The << operator shifts x left by a number of bits computed as described below.

The high-order bits of x are discarded, the remaining bits are shifted left, and the low-order empty bit positions are set to zero.

  • Shift right:
int operator >>(int x, int count);
uint operator >>(uint x, int count);
long operator >>(long x, int count);
ulong operator >>(ulong x, int count);

The >> operator shifts x right by a number of bits computed as described below.

When x is of type int or long, the low-order bits of x are discarded, the remaining bits are shifted right, and the high-order empty bit positions are set to zero if x is non-negative and set to one if x is negative.

When x is of type uint or ulong, the low-order bits of x are discarded, the remaining bits are shifted right, and the high-order empty bit positions are set to zero.

For the predefined operators, the number of bits to shift is computed as follows:

  • When the type of x is int or uint, the shift count is given by the low-order five bits of count. In other words, the shift count is computed from count & 0x1F.
  • When the type of x is long or ulong, the shift count is given by the low-order six bits of count. In other words, the shift count is computed from count & 0x3F.

If the resulting shift count is zero, the shift operators simply return the value of x.

Shift operations never cause overflows and produce the same results in checked and unchecked contexts.

When the left operand of the >> operator is of a signed integral type, the operator performs an arithmetic shift right wherein the value of the most significant bit (the sign bit) of the operand is propagated to the high-order empty bit positions. When the left operand of the >> operator is of an unsigned integral type, the operator performs a logical shift right wherein high-order empty bit positions are always set to zero. To perform the opposite operation of that inferred from the operand type, explicit casts can be used. For example, if x is a variable of type int, the operation (int)((uint)x >> y) performs a logical shift right of x.


7.9 Relational operators

The ==, !=, <, >, <=, >=, and is operators are called the relational operators.

relational-expression:
shift-expression
relational-expression < shift-expression
relational-expression > shift-expression
relational-expression <= shift-expression
relational-expression >= shift-expression
relational-expression is reference-type
equality-expression:
relational-expression
equality-expression == relational-expression
equality-expression != relational-expression

The is operator is described in §7.9.9.

The ==, !=, <, >, <= and >= operators are comparison operators. For an operation of the form x op y, where op is a comparison operator, overload resolution (§7.2.4) is applied to select a specific operator implementation. The operands are converted to the parameter types of the selected operator, and the type of the result is the return type of the operator.

The predefined comparison operators are described in the following sections. All predefined comparison operators return a result of type bool, as described in the following table.

Operation | Result
x == y | true if x is equal to y, false otherwise
x != y | true if x is not equal to y, false otherwise
x < y | true if x is less than y, false otherwise
x > y | true if x is greater than y, false otherwise
x <= y | true if x is less than or equal to y, false otherwise
x >= y | true if x is greater than or equal to y, false otherwise


7.9.1 Integer comparison operators

The predefined integer comparison operators are:

bool operator ==(int x, int y);
bool operator ==(uint x, uint y);
bool operator ==(long x, long y);
bool operator ==(ulong x, ulong y);
bool operator !=(int x, int y);
bool operator !=(uint x, uint y);
bool operator !=(long x, long y);
bool operator !=(ulong x, ulong y);
bool operator <(int x, int y);
bool operator <(uint x, uint y);
bool operator <(long x, long y);
bool operator <(ulong x, ulong y);
bool operator >(int x, int y);
bool operator >(uint x, uint y);
bool operator >(long x, long y);
bool operator >(ulong x, ulong y);
bool operator <=(int x, int y);
bool operator <=(uint x, uint y);
bool operator <=(long x, long y);
bool operator <=(ulong x, ulong y);
bool operator >=(int x, int y);
bool operator >=(uint x, uint y);
bool operator >=(long x, long y);
bool operator >=(ulong x, ulong y);

Each of these operators compare the numeric values of the two integer operands and return a bool value that indicates whether the particular relation is true or false.


7.9.2 Floating-point comparison operators

The predefined floating-point comparison operators are:

bool operator ==(float x, float y);
bool operator ==(double x, double y);
bool operator !=(float x, float y);
bool operator !=(double x, double y);
bool operator <(float x, float y);
bool operator <(double x, double y);
bool operator >(float x, float y);
bool operator >(double x, double y);
bool operator <=(float x, float y);
bool operator <=(double x, double y);
bool operator >=(float x, float y);
bool operator >=(double x, double y);

The operators compare the operands according to the rules of the IEEE 754 standard:

  • If either operand is NaN, the result is false for all operators except !=, and true for the != operator. For any two operands, x != y always produces the same result as !(x == y). However, when one or both operands are NaN, the <, >, <=, and >= operators do not produce the same results as the logical negation of the opposite operator. For example, if either of x and y is NaN, then x < y is false, but !(x >= y) is true.
  • When neither operand is NaN, the operators compare the values of the two floating-point operands with respect to the ordering
–8 < –max < ... < –min < –0.0 == +0.0 < +min < ... < +max < +8

where min and max are the smallest and largest positive finite values that can be represented in the given floating-point format. Notable effects of this ordering are:

  • Negative and positive zero are considered equal.
  • A negative infinity is considered less than all other values, but equal to another negative infinity.
  • A positive infinity is considered greater than all other values, but equal to another positive infinity.


7.9.3 Decimal comparison operators

The predefined decimal comparison operators are:

bool operator ==(decimal x, decimal y);
bool operator !=(decimal x, decimal y);
bool operator <(decimal x, decimal y);
bool operator >(decimal x, decimal y);
bool operator <=(decimal x, decimal y);
bool operator >=(decimal x, decimal y);

Each of these operators compare the numeric values of the two decimal operands and return a bool value that indicates whether the particular relation is true or false.


7.9.4 Boolean equality operators

The predefined boolean equality operators are:

bool operator ==(bool x, bool y);
bool operator !=(bool x, bool y);

The result of == is true if both x and y are true or if both x and y are false. Otherwise, the result is false.

The result of != is false if both x and y are true or if both x and y are false. Otherwise, the result is true. When the operands are of type bool, the != operator produces the same result as the ^ operator.


7.9.5 Enumeration comparison operators

Every enumeration type implicitly provides the following predefined comparison operators:

bool operator ==(E x, E y);
bool operator !=(E x, E y);
bool operator <(E x, E y);
bool operator >(E x, E y);
bool operator <=(E x, E y);
bool operator >=(E x, E y);

The result of evaluating x op y, where x and y are expressions of an enumeration type E with an underlying type U, and op is one of the comparison operators, is exactly the same as evaluating ((U)x) op ((U)y). In other words, the enumeration type comparison operators simply compare the underlying integral values of the two operands.


7.9.6 Reference type equality operators

The predefined reference type equality operators are:

bool operator ==(object x, object y);
bool operator !=(object x, object y);

The operators return the result of comparing the two references for equality or non-equality.

Since the predefined reference type equality operators accept operands of type object, they apply to all types that do not declare applicable operator == and operator != members. Conversely, any applicable user-defined equality operators effectively hide the predefined reference type equality operators.

The predefined reference type equality operators require the operands to be reference-type values or the value null, and furthermore require that an implicit conversion exists from the type of either operand to the type of the other operand. Unless both of these conditions are true, a compile-time error occurs. Notable implications of these rules are:

  • It is an error to use the predefined reference type equality operators to compare two references that are known to be different at compile-time. For example, if the compile-time types of the operands are two class types A and B, and if neither A nor B derives from the other, then it would be impossible for the two operands to reference the same object. Thus, the operation is considered a compile-time error.
  • The predefined reference type equality operators do not permit value type operands to be compared. Therefore, unless a struct type declares its own equality operators, it is not possible to compare values of that struct type.
  • The predefined reference type equality operators never cause boxing operations to occur for their operands. It would be meaningless to perform such boxing operations, since references to the newly allocated boxed instances would necessarily differ from all other references.

For an operation of the form x == y or x != y, if any applicable operator == or operator != exists, the operator overload resolution (§7.2.4) rules will select that operator instead of the predefined reference type equality operator. However, it is always possible to select the reference type equality operator by explicitly casting one or both of the operands to type object. The example

class Test
{
   static void Main() {
      string s = "Test";
      string t = string.Copy(s);
      Console.WriteLine(s == t);
      Console.WriteLine((object)s == t);
      Console.WriteLine(s == (object)t);
      Console.WriteLine((object)s == (object)t);
   }
}

produces the output

True
False
False
False

The s and t variables refer to two distinct string instances containing the same characters. The first comparison outputs True because the predefined string equality operator (§7.9.7) is selected when both operands are of type string. The remaining comparisons all output False because the predefined reference type equality operator is selected when one or both of the operands are of type object.

Note that the above technique is not meaningful for value types. The example

class Test
{
   static void Main() {
      int i = 123;
      int j = 123;
      Console.WriteLine((object)i == (object)j);
   }
}

outputs False because the casts create references to two separate instances of boxed int values.


7.9.7 String equality operators

The predefined string equality operators are:

bool operator ==(string x, string y);
bool operator !=(string x, string y);

Two string values are considered equal when one of the following is true:

  • Both values are null.
  • Both values are non-null references to string instances that have identical lengths and identical characters in each character position.

The string equality operators compare string values rather than string references. When two separate string instances contain the exact same sequence of characters, the values of the strings are equal, but the references are different. As described in §7.9.6, the reference type equality operators can be used to compare string references instead of string values.


7.9.8 Delegate equality operators

Every delegate type D implicitly provides the following predefined comparison operators:

bool operator ==(System.Delegate x, D y);
bool operator ==(D x, System.Delegate y);
bool operator !=(System.Delegate x, D y);
bool operator !=(D x, System.Delegate y);

Two delegate instances are considered equal as follows:

  • If either of the delegate instances is null, they are equal if and only if both are null.
  • If either of the delegate instances was instantiated with another delegate, they are equal if and only if both were instantiated on the same delegate instance. Otherwise,
  • If either of the delegate instances is a non-multicast delegate, the are equal if and only if both are non-multicast delegates, and either:
    • both refer to the same static method, or
    • both refer to the same non-static method on the same target object.
  • If either of the delegate instances is a multi-cast delegate, they are equal if and only if their invocation lists are the same length, and each delegate in one’s invocation list is equal to the corresponding delegate, in order, in the other’s invocation list.

Note that delegates of different types can be considered equal by the above definition, as long as they have the same return type and parameter types.


7.9.9 The is operator

The is operator is used to check whether the run-time type of an object is compatible with a given type. In an operation of the form e is T, e must be an expression of a reference-type and T must be a reference-type. If this is not the case, a compile-time error occurs.

The operation e is T returns true if e is not null and if an implicit reference conversion (§6.1.4) from the run-time type of the instance referenced by e to the type given by T exists. In other words, e is T checks that e is not null and that a cast-expression (§7.6.8) of the form (T)(e) will complete without throwing an exception.

If e is T is known at compile-time to always be true or always be false, a compile-time error occurs. The operation is known to always be true if an implicit reference conversion exists from the compile-time type of e to T. The operation is known to always be false if no implicit or explicit reference conversion exists from the compile-time type of e to T.


7.10 Logical operators

The &, ^, and | operators are called the logical operators.

and-expression:
equality-expression
and-expression & equality-expression
exclusive-or-expression:
and-expression
exclusive-or-expression ^ and-expression
inclusive-or-expression:
exclusive-or-expression
inclusive-or-expression | exclusive-or-expression

For an operation of the form x op y, where op is one of the logical operators, overload resolution (§7.2.4) is applied to select a specific operator implementation. The operands are converted to the parameter types of the selected operator, and the type of the result is the return type of the operator.

The predefined logical operators are described in the following sections.


7.10.1 Integer logical operators

The predefined integer logical operators are:

int operator &(int x, int y);
uint operator &(uint x, uint y);
long operator &(long x, long y);
ulong operator &(ulong x, ulong y);
int operator |(int x, int y);
uint operator |(uint x, uint y);
long operator |(long x, long y);
ulong operator |(ulong x, ulong y);
int operator ^(int x, int y);
uint operator ^(uint x, uint y);
long operator ^(long x, long y);
ulong operator ^(ulong x, ulong y);

The & operator computes the bitwise logical AND of the two operands, the | operator computes the bitwise logical OR of the two operands, and the ^ operator computes the bitwise logical exclusive OR of the two operands. No overflows are possible from these operations.


7.10.2 Enumeration logical operators

Every enumeration type E implicitly provides the following predefined logical operators:

E operator &(E x, E y);
E operator |(E x, E y);
E operator ^(E x, E y);

The result of evaluating x op y, where x and y are expressions of an enumeration type E with an underlying type U, and op is one of the logical operators, is exactly the same as evaluating (E)((U)x) op ((U)y). In other words, the enumeration type logical operators simply perform the logical operation on the underlying type of the two operands.


7.10.3 Boolean logical operators

The predefined boolean logical operators are:

bool operator &(bool x, bool y);
bool operator |(bool x, bool y);
bool operator ^(bool x, bool y);

The result of x & y is true if both x and y are true. Otherwise, the result is false.

The result of x | y is true if either x or y is true. Otherwise, the result is false.

The result of x ^ y is true if x is true and y is false, or x is false and y is true. Otherwise, the result is false. When the operands are of type bool, the ^ operator computes the same result as the != operator.


7.11 Conditional logical operators

The && and || operators are called the conditional logical operators. They are at times also called the “short-circuiting” logical operators.

conditional-and-expression:
inclusive-or-expression
conditional-and-expression && inclusive-or-expression
conditional-or-expression:
conditional-and-expression
conditional-or-expression || conditional-and-expression

The && and || operators are conditional versions of the & and | operators:

  • The operation x && y corresponds to the operation x & y, except that y is evaluated only if x is true.
  • The operation x || y corresponds to the operation x | y, except that y is evaluated only if x is false.

An operation of the form x && y or x || y is processed by applying overload resolution (§7.2.4) as if the operation was written x & y or x | y. Then,

  • If overload resolution fails to find a single best operator, or if overload resolution selects one of the predefined integer logical operators, an error occurs.
  • Otherwise, if the selected operator is one of the predefined boolean logical operators (§7.10.2), the operation is processed as described in §7.11.1.
  • Otherwise, the selected operator is a user-defined operator, and the operation is processed as described in §7.11.2.

It is not possible to directly overload the conditional logical operators. However, because the conditional logical operators are evaluated in terms of the regular logical operators, overloads of the regular logical operators are, with certain restrictions, also considered overloads of the conditional logical operators. This is described further in §7.11.2.


7.11.1 Boolean conditional logical operators

When the operands of && or || are of type bool, or when the operands are of types that do not define an applicable operator & or operator |, but do define implicit conversions to bool, the operation is processed as follows:

  • The operation x && y is evaluated as x? y: false. In other words, x is first evaluated and converted to type bool. Then, if x is true, y is evaluated and converted to type bool, and this becomes the result of the operation. Otherwise, the result of the operation is false.
  • The operation x || y is evaluated as x? true: y. In other words, x is first evaluated and converted to type bool. Then, if x is true, the result of the operation is true. Otherwise, y is evaluated and converted to type bool, and this becomes the result of the operation.


7.11.2 User-defined conditional logical operators

When the operands of && or || are of types that declare an applicable user-defined operator & or operator |, both of the following must be true, where T is the type in which the selected operator is declared:

  • The return type and the type of each parameter of the selected operator must be T. In other words, the operator must compute the logical AND or the logical OR of two operands of type T, and must return a result of type T.
  • T must contain declarations of operator true and operator false.

A compile-time error occurs if either of these requirements is not satisfied. Otherwise, the && or || operation is evaluated by combining the user-defined operator true or operator false with the selected user-defined operator:

  • The operation x && y is evaluated as T.false(x)? x: T.&(x, y), where T.false(x) is an invocation of the operator false declared in T, and T.&(x, y) is an invocation of the selected operator &. In other words, x is first evaluated and operator false is invoked on the result to determine if x is definitely false. Then, if x is definitely false, the result of the operation is the value previously computed for x. Otherwise, y is evaluated, and the selected operator & is invoked on the value previously computed for x and the value computed for y to produce the result of the operation.
  • The operation x || y is evaluated as T.true(x)? x: T.|(x, y), where T.true(x) is an invocation of the operator true declared in T, and T.|(x, y) is an invocation of the selected operator |. In other words, x is first evaluated and operator true is invoked on the result to determine if x is definitely true. Then, if x is definitely true, the result of the operation is the value previously computed for x. Otherwise, y is evaluated, and the selected operator | is invoked on the value previously computed for x and the value computed for y to produce the result of the operation.

In either of these operations, the expression given by x is only evaluated once, and the expression given by y is either not evaluated or evaluated exactly once.

For an example of a type that implements operator true and operator false, see §11.3.2.


7.12 Conditional operator

The ?: operator is called the conditional operator. It is at times also called the ternary operator.

conditional-expression:
conditional-or-expression
conditional-or-expression ? expression : expression

A conditional expression of the form b? x: y first evaluates the condition b. Then, if b is true, x is evaluated and becomes the result of the operation. Otherwise, y is evaluated and becomes the result of the operation. A conditional expression never evaluates both x and y.

The conditional operator is right-associative, meaning that operations are grouped from right to left. For example, an expression of the form a? b: c? d: e is evaluated as a? b: (c? d: e).

The first operand of the ?: operator must be an expression of a type that can be implicitly converted to bool, or an expression of a type that implements operator true. If neither of these requirements are satisfied, a compile-time error occurs.

The second and third operands of the ?: operator control the type of the conditional expression. Let X and Y be the types of the second and third operands. Then,

  • If X and Y are the same type, then this is the type of the conditional expression.
  • Otherwise, if an implicit conversion (§6.1) exists from X to Y, but not from Y to X, then Y is the type of the conditional expression.
  • Otherwise, if an implicit conversion (§6.1) exists from Y to X, but not from X to Y, then X is the type of the conditional expression.
  • Otherwise, no expression type can be determined, and a compile-time error occurs.

The run-time processing of a conditional expression of the form b? x: y consists of the following steps:

  • First, b is evaluated, and the bool value of b is determined:
    • If an implicit conversion from the type of b to bool exists, then this implicit conversion is performed to produce a bool value.
    • Otherwise, the operator true defined by the type of b is invoked to produce a bool value.
  • If the bool value produced by the step above is true, then x is evaluated and converted to the type of the conditional expression, and this becomes the result of the conditional expression.
  • Otherwise, y is evaluated and converted to the type of the conditional expression, and this becomes the result of the conditional expression.


7.13 Assignment operators

The assignment operators assign a new value to a variable, a property, or an indexer element.

assignment:
unary-expression assignment-operator expression
assignment-operator: one of
= += -= *= /= %= &= |= ^= <<= >>=

The left operand of an assignment must be an expression classified as a variable, a property access, or an indexer access.

The = operator is called the simple assignment operator. It assigns the value of the right operand to the variable, property, or indexer element given by the left operand. The simple assignment operator is described in §7.13.1.

The operators formed by prefixing a binary operator with an = character are called the compound assignment operators. These operators perform the indicated operation on the two operands, and then assign the resulting value to the variable, property, or indexer element given by the left operand. The compound assignment operators are described in §7.13.2.

The assignment operators are right-associative, meaning that operations are grouped from right to left. For example, an expression of the form a = b = c is evaluated as a = (b = c).


7.13.1 Simple assignment

The = operator is called the simple assignment operator. In a simple assignment, the right operand must be an expression of a type that is implicitly convertible to the type of the left operand. The operation assigns the value of the right operand to the variable, property, or indexer element given by the left operand.

The result of a simple assignment expression is the value assigned to the left operand. The result has the same type as the left operand and is always classified as a value.

If the left operand is a property or indexer access, the property or indexer must have a set accessor. If this is not the case, a compile-time error occurs.

The run-time processing of a simple assignment of the form x = y consists of the following steps:

  • If x is classified as a variable:
    • x is evaluated to produce the variable.
    • y is evaluated and, if required, converted to the type of x through an implicit conversion (§6.1).
    • If the variable given by x is an array element of a reference-type, a run-time check is performed to ensure that the value computed for y is compatible with the array instance of which x is an element. The check succeeds if y is null, or if an implicit reference conversion (§6.1.4) exists from the actual type of the instance referenced by y to the actual element type of the array instance containing x. Otherwise, an ArrayTypeMismatchException is thrown.
    • The value resulting from the evaluation and conversion of y is stored into the location given by the evaluation of x.
  • If x is classified as a property or indexer access:
    • The instance expression (if x is not static) and the argument list (if x is an indexer access) associated with x are evaluated, and the results are used in the subsequent set accessor invocation.
    • y is evaluated and, if required, converted to the type of x through an implicit conversion (§6.1).
    • The set accessor of x is invoked with the value computed for y as its value argument.

The array co-variance rules (§12.5) permit a value of an array type A[] to be a reference to an instance of an array type B[], provided an implicit reference conversion exists from B to A. Because of these rules, assignment to an array element of a reference-type requires a run-time check to ensure that the value being assigned is compatible with the array instance. In the example

string[] sa = new string[10];
object[] oa = sa;
oa[0] = null;               // Ok
oa[1] = "Hello";            // Ok
oa[2] = new ArrayList();   // ArrayTypeMismatchException

the last assignment causes an ArrayTypeMismatchException to be thrown because an instance of ArrayList cannot be stored in an element of a string[].

When a property or indexer declared in a struct-type is the target of an assignment, the instance expression associated with the property or indexer access must be classified as a variable. If the instance expression is classified as a value, a compile-time error occurs.

Given the declarations:

struct Point
{
   int x, y;
   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }
   public int X {
      get { return x; }
      set { x = value; }
   }
   public int Y {
      get { return y; }
      set { y = value; }
   }
}
struct Rectangle
{
   Point a, b;
   public Rectangle(Point a, Point b) {
      this.a = a;
      this.b = b;
   }
   public Point A {
      get { return a; }
      set { a = value; }
   }
   public Point B {
      get { return b; }
      set { b = value; }
   }
}

in the example

Point p = new Point();
p.X = 100;
p.Y = 100;
Rectangle r = new Rectangle();
r.A = new Point(10, 10);
r.B = p;

the assignments to p.X, p.Y, r.A, and r.B are permitted because p and r are variables. However, in the example

Rectangle r = new Rectangle();
r.A.X = 10;
r.A.Y = 10;
r.B.X = 100;
r.B.Y = 100;

the assignments are all invalid, since r.A and r.B are not variables.


7.13.2 Compound assignment

An operation of the form x op= y is processed by applying binary operator overload resolution (§7.2.4) as if the operation was written x op y. Then,

  • If the return type of the selected operator is implicitly convertible to the type of x, the operation is evaluated as x = x op y, except that x is evaluated only once.
  • Otherwise, if the selected operator is a predefined operator, if the return type of the selected operator is explicitly convertible to the type of x, and if y is implicitly convertible to the type of x, then the operation is evaluated as x = (T)(x op y), where T is the type of x, except that x is evaluated only once.
  • Otherwise, the compound assignment is invalid, and a compile-time error occurs.

The term “evaluated only once” means that in the evaluation of x op y, the results of any constituent expressions of x are temporarily saved and then reused when performing the assignment to x. For example, in the assignment A()[B()] += C(), where A is a method returning int[], and B and C are methods returning int, the methods are invoked only once, in the order A, B, C.

When the left operand of a compound assignment is a property access or indexer access, the property or indexer must have both a get accessor and a set accessor. If this is not the case, a compile-time error occurs.

The second rule above permits x op= y to be evaluated as x = (T)(x op y) in certain contexts. The rule exists such that the predefined operators can be used as compound operators when the left operand is of type sbyte, byte, short, ushort, or char. Even when both arguments are of one of those types, the predefined operators produce a result of type int, as described in §7.2.6.2. Thus, without a cast it would not be possible to assign the result to the left operand.

The intuitive effect of the rule for predefined operators is simply that x op= y is permitted if both of x op y and x = y are permitted. In the example

byte b = 0;
char ch = '\0';
int i = 0;
b += 1;            // Ok
b += 1000;         // Error, b = 1000 not permitted
b += i;            // Error, b = i not permitted
b += (byte)i;      // Ok
ch += 1;            // Error, ch = 1 not permitted
ch += (char)1;      // Ok

the intuitive reason for each error is that a corresponding simple assignment would also have been an error.


7.13.3 Event assignment

© 1987-2000 Microsoft Corporation. All rights reserved.


7.14 Expression

An expression is either a conditional-expression or an assignment.

expression:
conditional-expression
assignment


7.15 Constant expressions

A constant-expression is an expression that can be fully evaluated at compile-time.

constant-expression:
expression

The type of a constant expression can be one of the following: sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, string, any enumeration type, or the null type. The following constructs are permitted in constant expressions:

  • Literals (including the null literal).
  • References to const members of class and struct types.
  • References to members of enumeration types.
  • Parenthesized sub-expressions.
  • Cast expressions, provided the target type is one of the types listed above.
  • The predefined +, –, !, and ~ unary operators.
  • The predefined +, –, *, /, %, <<, >>, &, |, ^, &&, ||, ==, !=, <, >, <=, and => binary operators, provided each operand is of a type listed above.
  • The ?: conditional operator.

Whenever an expression is of one of the types listed above and contains only the constructs listed above, the expression is evaluated at compile-time. This is true even if the expression is a sub-expression of a larger expression that contains non-constant constructs.

The compile-time evaluation of constant expressions uses the same rules as run-time evaluation of non-constant expressions, except that where run-time evaluation would have thrown an exception, compile-time evaluation causes a compile-time error to occur.

Unless a constant expression is explicitly placed in an unchecked context, overflows that occur in integral-type arithmetic operations and conversions during the compile-time evaluation of the expression always cause compile-time errors (§7.5.12).

Constant expressions occur in the contexts listed below. In these contexts, an error occurs if an expression cannot be fully evaluated at compile-time.

  • Constant declarations (§10.3).
  • Enumeration member declarations (§14.2).
  • case labels of a switch statement (§8.7.2).
  • goto case statements (§8.9.3).
  • Dimension lengths in an array creation expression (§7.5.10.2) that includes an initializer.
  • Attributes (§17).

An implicit constant expression conversion (§6.1.6) permits a constant expression of type int to be converted to sbyte, byte, short, ushort, uint, or ulong, provided the value of the constant expression is within the range of the destination type.


7.16 Boolean expressions

A boolean-expression is an expression that yields a result of type bool.

boolean-expression:
expression

The controlling conditional expression of an if-statement (§8.7.1), while-statement (§8.8.1), do-statement (§8.8.2), or for-statement (§8.8.3) is a boolean-expression. The controlling conditional expression of the ?: operator (§7.12) follows the same rules as a boolean-expression, but for reasons of operator precedence is classified as a conditional-or-expression.

A boolean-expression is required to be of a type that can be implicitly converted to bool or of a type that implements operator true. If neither of these requirements are satisfied, a compile-time error occurs.

When a boolean expression is of a type that cannot be implicitly converted to bool but does implement operator true, then following evaluation of the expression, the operator true implementation provided by the type is invoked to produce a bool value.

The DBBool struct type in §11.3.2 provides an example of a type that implements operator true.


8. Statements

C# provides a variety of statements. Most of these statements will be familiar to developers who have programmed in C and C++.

statement:
labeled-statement
declaration-statement
embedded-statement
embedded-statement:
block
empty-statement
expression-statement
selection-statement
iteration-statement
jump-statement
try-statement
checked-statement
unchecked-statement
lock-statement
using-statement

The embedded-statement nonterminal is used for statements that appear within other statements. The use of embedded-statement rather than statement excludes the use of declaration statements and labeled statements in these contexts. For example, the code

void F(bool b) {
   if (b)
      int i = 44;
}

is in error because an if statement requires an embedded-statement rather than a statement for its if branch. If this code were permitted, then the variable i would be declared, but it could never be used.


8.1 End points and reachability

Every statement has an end point. In intuitive terms, the end point of a statement is the location that immediately follows the statement. The execution rules for composite statements (statements that contain embedded statements) specify the action that is taken when control reaches the end point of an embedded statement. For example, when control reaches the end point of a statement in a block, control is transferred to the next statement in the block.

If a statement can possibly be reached by execution, the statement is said to be reachable. Conversely, if there is no possibility that a statement will be executed, the statement is said to be unreachable.

In the example

void F() {
   Console.WriteLine("reachable");
   goto Label;
   Console.WriteLine("unreachable");
   Label:
   Console.WriteLine("reachable");
}

the second Console.WriteLine invocation is unreachable because there is no possibility that the statement will be executed.

A warning is reported if the compiler determines that a statement is unreachable. It is specifically not an error for a statement to be unreachable.

To determine whether a particular statement or end point is reachable, the compiler performs flow analysis according to the reachability rules defined for each statement. The flow analysis takes into account the values of constant expressions (§7.15) that control the behavior of statements, but the possible values of non-constant expressions are not considered. In other words, for purposes of control flow analysis, a non-constant expression of a given type is considered to have any possible value of that type.

In the example

void F() {
   const int i = 1;
   if (i == 2) Console.WriteLine("unreachable");
}

the boolean expression of the if statement is a constant expression because both operands of the == operator are constants. The constant expression is evaluated at compile-time, producing the value false, and the Console.WriteLine invocation is therefore considered unreachable. However, if i is changed to be a local variable

void F() {
   int i = 1;
   if (i == 2) Console.WriteLine("reachable");
}

the Console.WriteLine invocation is considered reachable, even though it will in reality never be executed.

The block of a function member is always considered reachable. By successively evaluating the reachability rules of each statement in a block, the reachability of any given statement can be determined.

In the example

void F(int x) {
   Console.WriteLine("start");
   if (x < 0) Console.WriteLine("negative");
}

the reachability of the second Console.WriteLine is determined as follows:

  • First, because the block of the F method is reachable, the first Console.WriteLine statement is reachable.
  • Next, because the first Console.WriteLine statement is reachable, its end point is reachable.
  • Next, because the end point of the first Console.WriteLine statement is reachable, the if statement is reachable.
  • Finally, because the boolean expression of the if statement does not have the constant value false, the second Console.WriteLine statement is reachable.

There are two situations in which it is an error for the end point of a statement to be reachable:

  • Because the switch statement does not permit a switch section to “fall through” to the next switch section, it is an error for the end point of the statement list of a switch section to be reachable. If this error occurs, it is typically an indication that a break statement is missing.
  • It is an error for the end point of the block of a function member that computes a value to be reachable. If this error occurs, it is typically an indication that a return statement is missing.


8.2 Blocks

A block permits multiple statements to be written in contexts where a single statement is allowed.

block:
{ statement-listopt }

A block consists of an optional statement-list (§8.2.1), enclosed in braces. If the statement list is omitted, the block is said to be empty.

A block may contain declaration statements (§8.5). The scope of a local variable or constant declared in a block extends from the declaration to the end of the block.

Within a block, the meaning of a name used in an expression context must always be the same (§7.5.2.1).

A block is executed as follows:

  • If the block is empty, control is transferred to the end point of the block.
  • If the block is not empty, control is transferred to the statement list. When and if control reaches the end point of the statement list, control is transferred to the end point of the block.

The statement list of a block is reachable if the block itself is reachable.

The end point of a block is reachable if the block is empty or if the end point of the statement list is reachable.


8.2.1 Statement lists

A statement list consists of one or more statements written in sequence. Statement lists occur in blocks (§8.2) and in switch-blocks (§8.7.2).

statement-list:
statement
statement-list statement

A statement list is executed by transferring control to the first statement. When and if control reaches the end point of a statement, control is transferred to the next statement. When and if control reaches the end point of the last statement, control is transferred to the end point of the statement list.

A statement in a statement list is reachable if at least one of the following is true:

  • The statement is the first statement and the statement list itself is reachable.
  • The end point of the preceding statement is reachable.
  • The statement is a labeled statement and the label is referenced by a reachable goto statement.

The end point of a statement list is reachable if the end point of the last statement in the list is reachable.


8.3 The empty statement

An empty-statement does nothing.

empty-statement:
;

An empty statement is used when there are no operations to perform in a context where a statement is required.

Execution of an empty statement simply transfers control to the end point of the statement. Thus, the end point of an empty statement is reachable if the empty statement is reachable.

An empty statement can be used when writing a while statement with a null body:

bool ProcessMessage() {...}
void ProcessMessages() {
   while (ProcessMessage())
      ;
}

Also, an empty statement can be used to declare a label just before the closing “}” of a block:

void F() {
   ...
if (done) goto exit;
   ...
exit: ;
}


8.4 Labeled statements

A labeled-statement permits a statement to be prefixed by a label. Labeled statements are permitted blocks, but are not permitted as embedded statements.

labeled-statement:
identifier : statement

A labeled statement declares a label with the name given by the identifier. The scope of a label is the block in which the label is declared, including any nested blocks. It is an error for two labels with the same name to have overlapping scopes.

A label can be referenced from goto statements (§8.9.3) within the scope of the label. This means that goto statements can transfer control inside blocks and out of blocks, but never into blocks.

Labels have their own declaration space and do not interfere with other identifiers. The example

int F(int x) {
   if (x >= 0) goto x;
   x = -x;
   x: return x;
}

is valid and uses the name x as both a parameter and a label.

Execution of a labeled statement corresponds exactly to execution of the statement following the label.

In addition to the reachability provided by normal flow of control, a labeled statement is reachable if the label is referenced by a reachable goto statement.


8.5 Declaration statements

A declaration-statement declares a local variable or constant. Declaration statements are permitted in blocks, but are not permitted as embedded statements.

declaration-statement:
local-variable-declaration ;
local-constant-declaration ;


8.5.1 Local variable declarations

A local-variable-declaration declares one or more local variables.

local-variable-declaration:
type variable-declarators
variable-declarators:
variable-declarator
variable-declarators , variable-declarator
variable-declarator:
identifier
identifier = variable-initializer
variable-initializer:
expression
array-initializer

The type of a local-variable-declaration specifies the type of the variables introduced by the declaration. The type is followed by a list of variable-declarators, each of which introduces a new variable. A variable-declarator consists of an identifier that names the variable, optionally followed by an “=” token and a variable-initializer that gives the initial value of the variable.

The value of a local variable is obtained in an expression using a simple-name (§7.5.2), and the value of a local variable is modified using an assignment (§7.13). A local variable must be definitely assigned (§5.3) at each location where its value is obtained.

The scope of a local variable starts immediately after its identifier in the declaration and extends to the end of the block containing the declaration. Within the scope of a local variable, it is an error to declare another local variable or constant with the same name.

A local variable declaration that declares multiple variables is equivalent to multiple declarations of single variables with the same type. Furthermore, a variable initializer in a local variable declaration corresponds exactly to an assignment statement that is inserted immediately after the declaration.

The example

void F() {
   int x = 1, y, z = x * 2;
}

corresponds exactly to

void F() {
   int x; x = 1;
   int y;
   int z; z = x * 2;
}


8.5.2 Local constant declarations

A local-constant-declaration declares one or more local constants.

local-constant-declaration:
const type constant-declarators
constant-declarators:
constant-declarator
constant-declarators , constant-declarator
constant-declarator:
identifier = constant-expression

The type of a local-constant-declaration specifies the type of the constants introduced by the declaration. The type is followed by a list of constant-declarators, each of which introduces a new constant. A constant-declarator consists of an identifier that names the constant, followed by an “=” token, followed by a constant-expression (§7.15) that gives the value of the constant.

The type and constant-expression of a local constant declaration must follow the same rules as those of a constant member declaration (§10.3).

The value of a local constant is obtained in an expression using a simple-name (§7.5.2).

The scope of a local constant extends from its declaration to the end of the block containing the declaration. The scope of a local constant does not include the constant-expression that provides its value. Within the scope of a local constant, it is an error to declare another local variable or constant with the same name.


8.6 Expression statements

An expression-statement evaluates a given expression. The value computed by the expression, if any, is discarded.

expression-statement:
statement-expression ;
statement-expression:
invocation-expression
object-creation-expression
assignment
post-increment-expression
post-decrement-expression
pre-increment-expression
pre-decrement-expression

Not all expressions are permitted as statements. In particular, expressions such as x + y and x == 1 that have no side-effects, but merely compute a value (which will be discarded), are not permitted as statements.

Execution of an expression statement evaluates the contained expression and then transfers control to the end point of the expression statement.


8.7 Selection statements

Selection statements select one of a number of possible statements for execution based on the value of a controlling expression.

selection-statement:
if-statement
switch-statement


8.7.1 The if statement

The if statement selects a statement for execution based on the value of a boolean expression.

if-statement:
if ( boolean-expression ) embedded-statement
if ( boolean-expression ) embedded-statement else embedded-statement
boolean-expression:
expression

An else part is associated with the nearest preceding if statement that does not already have an else part. Thus, an if statement of the form

if (x) if (y) F(); else G();

is equivalent to

if (x) {
   if (y) {
      F();
   }
   else {
      G();
   }
}

An if statement is executed as follows:

  • The boolean-expression (§7.16) is evaluated.
  • If the boolean expression yields true, control is transferred to the first embedded statement. When and if control reaches the end point of that statement, control is transferred to the end point of the if statement.
  • If the boolean expression yields false and if an else part is present, control is transferred to the second embedded statement. When and if control reaches the end point of that statement, control is transferred to the end point of the if statement.
  • If the boolean expression yields false and if an else part is not present, control is transferred to the end point of the if statement.

The first embedded statement of an if statement is reachable if the if statement is reachable and the boolean expression does not have the constant value false.

The second embedded statement of an if statement, if present, is reachable if the if statement is reachable and the boolean expression does not have the constant value true.

The end point of an if statement is reachable if the end point of at least one of its embedded statements is reachable. In addition, the end point of an if statement with no else part is reachable if the if statement is reachable and the boolean expression does not have the constant value true.


8.7.2 The switch statement

The switch statement executes the statements that are associated with the value of the controlling expression.

switch-statement:
switch ( expression ) switch-block
switch-block:
{ switch-sectionsopt }
switch-sections:
switch-section
switch-sections switch-section
switch-section:
switch-labels statement-list
switch-labels:
switch-label
switch-labels switch-label
switch-label:
case constant-expression :
default :

A switch-statement consists of the keyword switch, followed by a parenthesized expression (called the switch expression), followed by a switch-block. The switch-block consists of zero or more switch-sections, enclosed in braces. Each switch-section consists of one or more switch-labels followed by a statement-list (§8.2.1).

The governing type of a switch statement is established by the switch expression. If the type of the switch expression is sbyte, byte, short, ushort, int, uint, long, ulong, char, string, or an enum-type, then that is the governing type of the switch statement. Otherwise, exactly one user-defined implicit conversion (§6.4) must exist from the type of the switch expression to one of the following possible governing types: sbyte, byte, short, ushort, int, uint, long, ulong, char, string. If no such implicit conversion exists, or if more that one such implicit conversion exists, a compile-time error occurs.

The constant expression of each case label must denote a value of a type that is implicitly convertible (§6.1) to the governing type of the switch statement. A compile-time error occurs if two or more case labels in the same switch statement specify the same constant value.

There can be at most one default label in a switch statement.

A switch statement is executed as follows:

  • The switch expression is evaluated and converted to the governing type.
  • If one of the constants specified in a case label is equal to the value of the switch expression, control is transferred to the statement list following the matched case label.
  • If no constant matches the value of the switch expression and if a default label is present, control is transferred to the statement list following the default label.
  • If no constant matches the value of the switch expression and if no default label is present, control is transferred to the end point of the switch statement.

If the end point of the statement list of a switch section is reachable, a compile-time error occurs. This is known as the “no fall through” rule. The example

switch (i) {
case 0:
   CaseZero();
   break;
case 1:
   CaseOne();
   break;
default:
   CaseOthers();
   break;
}

is valid because no switch section has a reachable end point. Unlike C and C++, execution of a switch section is not permitted to “fall through” to the next switch section, and the example

switch (i) {
case 0:
   CaseZero();
case 1:
   CaseZeroOrOne();
default:
   CaseAny();
}

is in error. When execution of a switch section is to be followed by execution of another switch section, an explicit goto case or goto default statement must be used:

switch (i) {
case 0:
   CaseZero();
   goto case 1;
case 1:
   CaseZeroOrOne();
   goto default;
default:
   CaseAny();
   break;
}

Multiple labels are permitted in a switch-section. The example

switch (i) {
case 0:
   CaseZero();
   break;
case 1:
   CaseOne();
   break;
case 2:
default:
   CaseTwo();
   break;
}

is legal. The example does not violate the "no fall through" rule because the labels case 2: and default: are part of the same switch-section.

The “no fall through” rule prevents a common class of bugs that occur in C and C++ when break statements are accidentally omitted. Also, because of this rule, the switch sections of a switch statement can be arbitrarily rearranged without affecting the behavior of the statement. For example, the sections of the switch statement above can be reversed without affecting the behavior of the statement:

switch (i) {
default:
   CaseAny();
   break;
case 1:
   CaseZeroOrOne();
   goto default;
case 0:
   CaseZero();
   goto case 1;
}

The statement list of a switch section typically ends in a break, goto case, or goto default statement, but any construct that renders the end point of the statement list unreachable is permitted. For example, a while statement controlled by the boolean expression true is known to never reach its end point. Likewise, a throw or return statement always transfer control elsewhere and never reaches its end point. Thus, the following example is valid:

switch (i) {
case 0:
   while (true) F();
case 1:
   throw new ArgumentException();
case 2:
   return;
}

The governing type of a switch statement may be the type string. For example:

void DoCommand(string command) {
   switch (command.ToLower()) {
   case "run":
      DoRun();
      break;
   case "save":
      DoSave();
      break;
   case "quit":
      DoQuit();
      break;
   default:
      InvalidCommand(command);
      break;
   }
}

Like the string equality operators (§7.9.7), the switch statement is case sensitive and will execute a given switch section only if the switch expression string exactly matches a case label constant. As illustrated by the example above, a switch statement can be made case insensitive by converting the switch expression string to lower case and writing all case label constants in lower case.

When the governing type of a switch statement is string, the value null is permitted as a case label constant.

A switch-block may contain declaration statements (§8.5). The scope of a local variable or constant declared in a switch block extends from the declaration to the end of the switch block.

Within a switch block, the meaning of a name used in an expression context must always be the same (§7.5.2.1).

The statement list of a given switch section is reachable if the switch statement is reachable and at least one of the following is true:

  • The switch expression is a non-constant value.
  • The switch expression is a constant value that matches a case label in the switch section.
  • The switch expression is a constant value that doesn’t match any case label, and the switch section contains the default label.
  • A switch label of the switch section is referenced by a reachable goto case or goto default statement.

The end point of a switch statement is reachable if at least one of the following is true:

  • The switch statement contains a reachable break statement that exits the switch statement.
  • The switch statement is reachable, the switch expression is a non-constant value, and no default label is present.
  • The switch statement is reachable, the switch expression is a constant value that doesn’t match any case label, and no default label is present.


8.8 Iteration statements

Iteration statements repeatedly execute an embedded statement.

iteration-statement:
while-statement
do-statement
for-statement
foreach-statement


8.8.1 The while statement

The while statement conditionally executes an embedded statement zero or more times.

while-statement:
while ( boolean-expression ) embedded-statement

A while statement is executed as follows:

  • The boolean-expression (§7.16) is evaluated.
  • If the boolean expression yields true, control is transferred to the embedded statement. When and if control reaches the end point of the embedded statement (possibly from execution of a continue statement), control is transferred to the beginning of the while statement.
  • If the boolean expression yields false, control is transferred to the end point of the while statement.

Within the embedded statement of a while statement, a break statement (§8.9.1) may be used to transfer control to the end point of the while statement (thus ending iteration of the embedded statement), and a continue statement (§8.9.2) may be used to transfer control to the end point of the embedded statement (thus performing another iteration of the while statement).

The embedded statement of a while statement is reachable if the while statement is reachable and the boolean expression does not have the constant value false.

The end point of a while statement is reachable if at least one of the following is true:

  • The while statement contains a reachable break statement that exits the while statement.
  • The while statement is reachable and the boolean expression does not have the constant value true.


8.8.2 The do statement

The do statement conditionally executes an embedded statement one or more times.

do-statement:
do embedded-statement while ( boolean-expression ) ;

A do statement is executed as follows:

  • Control is transferred to the embedded statement.
  • When and if control reaches the end point of the embedded statement (possibly from execution of a continue statement), the boolean-expression (§7.16) is evaluated. If the boolean expression yields true, control is transferred to the beginning of the do statement. Otherwise, control is transferred to the end point of the do statement.

Within the embedded statement of a do statement, a break statement (§8.9.1) may be used to transfer control to the end point of the do statement (thus ending iteration of the embedded statement), and a continue statement (§8.9.2) may be used to transfer control to the end point of the embedded statement (thus performing another iteration of the do statement).

The embedded statement of a do statement is reachable if the do statement is reachable.

The end point of a do statement is reachable if at least one of the following is true:

  • The do statement contains a reachable break statement that exits the do statement.
  • The end point of the embedded statement is reachable and the boolean expression does not have the constant value true.


8.8.3 The for statement

The for statement evaluates a sequence of initialization expressions and then, while a condition is true, repeatedly executes an embedded statement and evaluates a sequence of iteration expressions.

for-statement:
for ( for-initializeropt ; for-conditionopt ; for-iteratoropt ) embedded-statement
for-initializer:
local-variable-declaration
statement-expression-list
for-condition:
boolean-expression
for-iterator:
statement-expression-list
statement-expression-list:
statement-expression
statement-expression-list , statement-expression

The for-initializer, if present, consists of either a local-variable-declaration (§8.5.1) or a list of statement-expressions (§8.6) separated by commas. The scope of a local variable declared by a for-initializer starts at the variable-declarator for the variable and extends to the end of the embedded statement. The scope includes the for-condition and the for-iterator.

The for-condition, if present, must be a boolean-expression (§7.16).

The for-iterator, if present, consists of a list of statement-expressions (§8.6) separated by commas.

A for statement is executed as follows:

  • If a for-initializer is present, the variable initializers or statement expressions are executed in the order they are written. This step is only performed once.
  • If a for-condition is present, it is evaluated.
  • If the for-condition is not present or if the evaluation yields true, control is transferred to the embedded statement. When and if control reaches the end point of the embedded statement (possibly from execution of a continue statement), the expressions of the for-iterator, if any, are evaluated in sequence, and then another iteration is performed, starting with evaluation of the for-condition in the step above.
  • If the for-condition is present and the evaluation yields false, control is transferred to the end point of the for statement.

Within the embedded statement of a for statement, a break statement (§8.9.1) may be used to transfer control to the end point of the for statement (thus ending iteration of the embedded statement), and a continue statement (§8.9.2) may be used to transfer control to the end point of the embedded statement (thus executing another iteration of the for statement).

The embedded statement of a for statement is reachable if one of the following is true:

  • The for statement is reachable and no for-condition is present.
  • The for statement is reachable and a for-condition is present and does not have the constant value false.

The end point of a for statement is reachable if at least one of the following is true:

  • The for statement contains a reachable break statement that exits the for statement.
  • The for statement is reachable and a for-condition is present and does not have the constant value true.


8.8.4 The foreach statement

The foreach statement enumerates the elements of a collection, executing an embedded statement for each element of the collection.

foreach-statement:
foreach ( type identifier in expression ) embedded-statement

The type and identifier of a foreach statement declare the iteration variable of the statement. The iteration variable corresponds to a read-only local variable with a scope that extends over the embedded statement. During execution of a foreach statement, the iteration variable represents the collection element for which an iteration is currently being performed. A compile-time error occurs if the embedded statement attempts to assign to the iteration variable or pass the iteration variable as a ref or out parameter.

The type of the expression of a foreach statement must be a collection type (as defined below), and an explicit conversion (§6.2) must exist from the element type of the collection to the type of the iteration variable.

A type C is said to be a collection type if all of the following are true:

  • C contains a public instance method with the signature GetEnumerator() that returns a struct-type, class-type, or interface-type, in the following called E.
  • E contains a public instance method with the signature MoveNext() and the return type bool.
  • E contains a public instance property named Current that permits reading. The type of this property is said to be the element type of the collection type.

The System.Array type (§12.1.1) is a collection type, and since all array types derive from System.Array, any array type expression is permitted in a foreach statement. For single-dimensional arrays, the foreach statement enumerates the array elements in increasing index order, starting with index 0 and ending with index Length – 1. For multi-dimensional arrays, the indices of the rightmost dimension are increased first.

A foreach statement is executed as follows:

  • The collection expression is evaluated to produce an instance of the collection type. This instance is referred to as c in the following. If c is of a reference-type and has the value null, a NullReferenceException is thrown.
  • An enumerator instance is obtained by evaluating the method invocation c.GetEnumerator(). The returned enumerator is stored in a temporary local variable, in the following referred to as e. It is not possible for the embedded statement to access this temporary variable. If e is of a reference-type and has the value null, a NullReferenceException is thrown.
  • The enumerator is advanced to the next element by evaluating the method invocation e.MoveNext().
  • If the value returned by e.MoveNext() is true, the following steps are performed:
    • The current enumerator value is obtained by evaluating the property access e.Current, and the value is converted to the type of the iteration variable by an explicit conversion (§6.2). The resulting value is stored in the iteration variable such that it can be accessed in the embedded statement.
    • Control is transferred to the embedded statement. When and if control reaches the end point of the embedded statement (possibly from execution of a continue statement), another foreach iteration is performed, starting with the step above that advances the enumerator.
  • If the value returned by e.MoveNext() is false, control is transferred to the end point of the foreach statement.

Within the embedded statement of a foreach statement, a break statement (§8.9.1) may be used to transfer control to the end point of the foreach statement (thus ending iteration of the embedded statement), and a continue statement (§8.9.2) may be used to transfer control to the end point of the embedded statement (thus executing another iteration of the foreach statement).

The embedded statement of a foreach statement is reachable if the foreach statement is reachable. Likewise, the end point of a foreach statement is reachable if the foreach statement is reachable.


8.9 Jump statements

Jump statements unconditionally transfer control.

jump-statement:
break-statement
continue-statement
goto-statement
return-statement
throw-statement

The location to which a jump statement transfers control is called the target of the jump statement.

When a jump statement occurs within a block, and when the target of the jump statement is outside that block, the jump statement is said to exit the block. While a jump statement may transfer control out of a block, it can never transfer control into a block.

Execution of jump statements is complicated by the presence of intervening try statements. In the absence of such try statements, a jump statement unconditionally transfers control from the jump statement to its target. In the presence of such intervening try statements, execution is more complex. If the jump statement exits one or more try blocks with associated finally blocks, control is initially transferred to the finally block of the innermost try statement. When and if control reaches the end point of a finally block, control is transferred to the finally block of the next enclosing try statement. This process is repeated until the finally blocks of all intervening try statements have been executed.

In the example

static void F() {
   while (true) {
      try {
         try {
            Console.WriteLine("Before break");
            break;
         }
         finally {
            Console.WriteLine("Innermost finally block");
         }
      }
      finally {
         Console.WriteLine("Outermost finally block");
      }
   }
   Console.WriteLine("After break");
}

the finally blocks associated with two try statements are executed before control is transferred to the target of the jump statement.


8.9.1 The break statement

The break statement exits the nearest enclosing switch, while, do, for, or foreach statement.

break-statement:
break ;

The target of a break statement is the end point of the nearest enclosing switch, while, do, for, or foreach statement. If a break statement is not enclosed by a switch, while, do, for, or foreach statement, a compile-time error occurs.

When multiple switch, while, do, for, or foreach statements are nested within each other, a break statement applies only to the innermost statement. To transfer control across multiple nesting levels, a goto statement (§8.9.3) must be used.

A break statement cannot exit a finally block (§8.10). When a break statement occurs within a finally block, the target of the break statement must be within the same finally block, or otherwise a compile-time error occurs.

A break statement is executed as follows:

  • If the break statement exits one or more try blocks with associated finally blocks, control is initially transferred to the finally block of the innermost try statement. When and if control reaches the end point of a finally block, control is transferred to the finally block of the next enclosing try statement. This process is repeated until the finally blocks of all intervening try statements have been executed.
  • Control is transferred to the target of the break statement.

Because a break statement unconditionally transfers control elsewhere, the end point of a break statement is never reachable.


8.9.2 The continue statement

The continue statement starts a new iteration of the nearest enclosing while, do, for, or foreach statement.

continue-statement:
continue ;

The target of a continue statement is the end point of the embedded statement of the nearest enclosing while, do, for, or foreach statement. If a continue statement is not enclosed by a while, do, for, or foreach statement, a compile-time error occurs.

When multiple while, do, for, or foreach statements are nested within each other, a continue statement applies only to the innermost statement. To transfer control across multiple nesting levels, a goto statement (§8.9.3) must be used.

A continue statement cannot exit a finally block (§8.10). When a continue statement occurs within a finally block, the target of the continue statement must be within the same finally block, or otherwise a compile-time error occurs.

A continue statement is executed as follows:

  • If the continue statement exits one or more try blocks with associated finally blocks, control is initially transferred to the finally block of the innermost try statement. When and if control reaches the end point of a finally block, control is transferred to the finally block of the next enclosing try statement. This process is repeated until the finally blocks of all intervening try statements have been executed.
  • Control is transferred to the target of the continue statement.

Because a continue statement unconditionally transfers control elsewhere, the end point of a continue statement is never reachable.


8.9.3 The goto statement

The goto statement transfers control to a statement that is marked by a label.

goto-statement:
goto identifier ;
goto case constant-expression ;
goto default ;

The target of a goto identifier statement is the labeled statement with the given label. If a label with the given name does not exist in the current function member, or if the goto statement is not within the scope of the label, a compile-time error occurs. This rule permits the use of a goto statement to transfer control out of a nested scope, but not into a nested scope. In the example

class Test
{
   static void Main(string[] args) {
      int i = 0;
      while (true) {
         Console.WriteLine(i++);
         if (i == 10)
             goto done;
      }
   done:
      Console.WriteLine("Done");
   }
}

a goto statement is used to transfer control out of a nested scope.

The target of a goto case statement is the statement list of the switch section in the nearest enclosing switch statement that contains a case label with the given constant value. If the goto case statement is not enclosed by a switch statement, if the constant-expression is not implicitly convertible (§6.1) to the governing type of the nearest enclosing switch statement, or if the nearest enclosing switch statement does not contain a case label with the given constant value, a compile-time error occurs.

The target of a goto default statement is the statement list of the switch section in the nearest enclosing switch statement (§8.7.2) that contains a default label. If the goto default statement is not enclosed by a switch statement, or if the nearest enclosing switch statement does not contain a default label, a compile-time error occurs.

A goto statement cannot exit a finally block (§8.10). When a goto statement occurs within a finally block, the target of the goto statement must be within the same finally block, or otherwise a compile-time error occurs.

A goto statement is executed as follows:

  • If the goto statement exits one or more try blocks with associated finally blocks, control is initially transferred to the finally block of the innermost try statement. When and if control reaches the end point of a finally block, control is transferred to the finally block of the next enclosing try statement. This process is repeated until the finally blocks of all intervening try statements have been executed.
  • Control is transferred to the target of the goto statement.

Because a goto statement unconditionally transfers control elsewhere, the end point of a goto statement is never reachable.


8.9.4 The return statement

The return statement returns control to the caller of the function member in which the return statement appears.

return-statement:
return expressionopt ;

A return statement with no expression can be used only in a function member that does not compute a value, that is, a method with the return type void, the set accessor of a property or indexer, a constructor, or a destructor.

A return statement with an expression can only be used only in a function member that computes a value, that is, a method with a non-void return type, the get accessor of a property or indexer, or a user-defined operator. An implicit conversion (§6.1) must exist from the type of the expression to the return type of the containing function member.

It is an error for a return statement to appear in a finally block (§8.10).

A return statement is executed as follows:

  • If the return statement specifies an expression, the expression is evaluated and the resulting value is converted to the return type of the containing function member by an implicit conversion. The result of the conversion becomes the value returned to the caller.
  • If the return statement is enclosed by one or more try blocks with associated finally blocks, control is initially transferred to the finally block of the innermost try statement. When and if control reaches the end point of a finally block, control is transferred to the finally block of the next enclosing try statement. This process is repeated until the finally blocks of all enclosing try statements have been executed.
  • Control is returned to the caller of the containing function member.

Because a return statement unconditionally transfers control elsewhere, the end point of a return statement is never reachable.


8.9.5 The throw statement

The throw statement throws an exception.

throw-statement:
throw expressionopt ;

A throw statement with an expression throws the exception produced by evaluating the expression. The expression must denote a value of the class type System.Exception or of a class type that derives from System.Exception. If evaluation of the expression produces null, a NullReferenceException is thrown instead.

A throw statement with no expression can be used only in a catch block. It re-throws the exception that is currently being handled by the catch block.

Because a throw statement unconditionally transfers control elsewhere, the end point of a throw statement is never reachable.

When an exception is thrown, control is transferred to the first catch clause in a try statement that can handle the exception. The process that takes place from the point of the exception being thrown to the point of transferring control to a suitable exception handler is known as exception propagation. Propagation of an exception consists of repeatedly evaluating the following steps until a catch clause that matches the exception is found. In the descriptions, the throw point is initially the location at which the exception is thrown.

  • In the current function member, each try statement that encloses the throw point is examined. For each statement S, starting with the innermost try statement and ending with the outermost try statement, the following steps are evaluated:
    • If the try block of S encloses the throw point and if S has one or more catch clauses, the catch clauses are examined in order of appearance to locate a suitable handler for the exception. The first catch clause that specifies the exception type or a base type of the exception type is considered a match. A general catch clause is considered a match for any exception type. If a matching catch clause is located, the exception propagation is completed by transferring control to the block of that catch clause.
    • Otherwise, if the try block or a catch block of S encloses the throw point and if S has a finally block, control is transferred to the finally block. If the finally block throws another exception, processing of the current exception is terminated. Otherwise, when control reaches the end point of the finally block, processing of the current exception is continued.
  • If an exception handler was not located in the current function member invocation, the function member invocation is terminated. The steps above are then repeated for the caller of the function member with a throw point corresponding to the statement from which the function member was invoked.
  • If the exception processing ends up terminating all function member invocations in the current thread or process, indicating that the thread or process has no handler for the exception, then the thread or process is itself terminated in an implementation-defined fashion.


8.10 The try statement

The try statement provides a mechanism for catching exceptions that occur during execution of a block. The try statement furthermore provides the ability to specify a block of code that is always executed when control leaves the try statement.

try-statement:
try block catch-clauses
try block finally-clause
try block catch-clauses finally-clause
catch-clauses:
specific-catch-clauses general-catch-clauseopt
specific-catch-clausesopt general-catch-clause
specific-catch-clauses:
specific-catch-clause
specific-catch-clauses specific-catch-clause
specific-catch-clause:
catch ( class-type identifieropt ) block
general-catch-clause:
catch block
finally-clause:
finally block

There are three possible forms of try statements:

  • A try block followed by one or more catch blocks.
  • A try block followed by a finally block.
  • A try block followed by one or more catch blocks followed by a finally block.

When a catch clause specifies a class-type, the type must be System.Exception or a type that derives from System.Exception.

When a catch clause specifies both a class-type and an identifier, an exception variable of the given name and type is declared. The exception variable corresponds to a read-only local variable with a scope that extends over the catch block. During execution of the catch block, the exception variable represents the exception currently being handled. A compile-time error occurs if a catch block attempts to assign to the exception variable or pass the exception variable as a ref or out parameter.

Unless a catch clause includes an exception variable name, it is impossible to access the exception object in the catch block.

A catch clause that specifies neither an exception type nor an exception variable name is called a general catch clause. A try statement can only have one general catch clause, and if one is present it must be the last catch clause. A general catch clause of the form

catch {...}

is precisely equivalent to

catch (System.Exception) {...}

An error occurs if a catch clause specifies a type that is the same as or derived from a type that was specified in an earlier catch clause. Because catch clauses are examined in order of appearance to locate a handler for an exception, without this restriction it would be possible to write unreachable catch clauses.

It is an error for a try statement to contain a general catch clause if the try statement also contains a catch clause for the System.Exception type.

Within a catch block, a throw statement (§8.9.5) with no expression can be used to re-throw the exception that is currently being handled by the catch block.

It is an error for a break, continue, or goto statement to transfer control out of a finally block. When a break, continue, or goto statement occurs in a finally block, the target of the statement must be within the same finally block, or otherwise a compile-time error occurs.

It is an error for a return statement to occur in a finally block.

A try statement is executed as follows:

  • Control is transferred to the try block.
  • When and if control reaches the end point of the try block:
    • If the try statement has a finally block, the finally block is executed.
    • Control is transferred to the end point of the try statement.
  • If an exception is propagated to the try statement during execution of the try block:
    • The catch clauses, if any, are examined in order of appearance to locate a suitable handler for the exception. The first catch clause that specifies the exception type or a base type of the exception type is considered a match. A general catch clause is considered a match for any exception type. If a matching catch clause is located:
      • If the matching catch clause declares an exception variable, the exception object is assigned to the exception variable.
      • Control is transferred to the matching catch block.
      • When and if control reaches the end point of the catch block:
        • If the try statement has a finally block, the finally block is executed.
        • Control is transferred to the end point of the try statement.
      • If an exception is propagated to the try statement during execution of the catch block:
        • If the try statement has a finally block, the finally block is executed.
        • The exception is propagated to the next enclosing try statement.
    • If the try statement has no catch clauses or if no catch clause matches the exception:
      • If the try statement has a finally block, the finally block is executed.
      • The exception is propagated to the next enclosing try statement.

The statements of a finally block are always executed when control leaves a try statement. This is true whether the control transfer occurs as a result of normal execution, as a result of executing a break, continue, goto, or return statement, or as a result of propagating an exception out of the try statement.

If an exception is thrown during execution of a finally block, the exception is propagated to the next enclosing try statement. If another exception was in the process of being propagated, that exception is lost. The process of propagating an exception is further discussed in the description of the throw statement (§8.9.5).

The try block of a try statement is reachable if the try statement is reachable.

A catch block of a try statement is reachable if the try statement is reachable.

The finally block of a try statement is reachable if the try statement is reachable.

The end point of a try statement is reachable if both of the following are true:

  • The end point of the try block is reachable or the end point of at least one catch block is reachable.
  • If a finally block is present, the end point of the finally block is reachable.


8.11 The checked and unchecked statements

The checked and unchecked statements are used to control the overflow checking context for integral-type arithmetic operations and conversions.

checked-statement:
checked block
unchecked-statement:
unchecked block

The checked statement causes all expressions in the block to be evaluated in a checked context, and the unchecked statement causes all expressions in the block to be evaluated in an unchecked context.

The checked and unchecked statements are precisely equivalent to the checked and unchecked operators (§7.5.12), except that they operate on blocks instead of expressions.


8.12 The lock statement

The lock statement obtains the mutual-exclusion lock for a given object, executes a statement, and then releases the lock.

lock-statement:
lock ( expression ) embedded-statement

The expression of a lock statement must denote a value of a reference-type. An implicit boxing conversion (§6.1.5) is never performed for the expression of a lock statement, and thus it is an error for the expression to denote a value of a value-type.

A lock statement of the form

lock (x) ...

where x is an expression of a reference-type, is precisely equivalent to

System.Threading.Monitor.Enter(x);
try {
   ...
}
finally {
   System.Threading.Monitor.Exit(x);
}

except that x is only evaluated once. The exact behavior of the Enter and Exit methods of the System.Threading.Monitor class is implementation-defined.

The System.Type object of a class can conveniently be used as the mutual-exclusion lock for static methods of the class. For example:

class Cache
{
   public static void Add(object x) {
      lock (typeof(Cache)) {
         ...
      }
   }
   public static void Remove(object x) {
      lock (typeof(Cache)) {
         ...
      }
   }
}


8.13 The using statement

The using statement obtains one or more resources, executes a statement, and then disposes of the resource.

using-statement:
using ( resource-acquisition ) embedded-statement
resource-acquisition:
local-variable-declaration
expression

A resource is a class or struct that implements System.IDisposable, which includes a single parameterless method named Dispose. Code that is using a resource can call Dispose to indicate that the resource is no longer needed. If Dispose is not called, then automatic disposal eventually occurs as a consequence of garbage collection.

If the form of resource-acquisition is local-variable-declaration then the type of the local-variable-declaration must be System.IDisposable or a type that can be implicitly converted to System.IDisposable. If the form of resource-acquisition is expression then this expression must be System.IDisposable or a type that can be implicitly converted to System.IDisposable.

Local variables declared in a resource-acquisition are read-only, and must include an initializer.

A using statement is translated into three parts: acquisition, usage, and disposal. Usage of the resource is enclosed in a try statement that includes a finally clause. This finally clause disposes of the resource. If a null resource is acquired, then no call to Dispose is made, and no exception is thrown.

For example, a using statement of the form

using (R r1 = new R ()) {
   r1.F();
}

is precisely equivalent to

R r1 = new R();
try {
   r1.F();
}
finally {
   if (r1 != null) ((IDisposable)r1).Dispose();
}

A resource-acquisition may acquire multiple resources of a given type. This is equivalent to nested using statements. For example, a using statement of the form

using (R r1 = new R(), r2 = new R()) {
   r1.F();
   r2.F();
}

is precisely equivalent to:

using (R r1 = new R())
   using (R r2 = new R()) {
      r1.F();
      r2.F();
   }

which is, by expansion, precisely equivalent to:

R r1 = new R();
try {
   R r2 = new R();
   try {
      r1.F();
      r2.F();
   }
   finally {
      if (r2 != null) ((IDisposable)r2).Dispose();
   }
}
finally {
   if (r1 != null) ((IDisposable)r1).Dispose();
}


9. Namespaces

C# programs are organized using namespaces. Namespaces are used both as an “internal” organization system for a program, and as an “external” organization system – a way of presenting program elements that are exposed to other programs.

Using directives are provided to facilitate the use of namespaces.


9.1 Compilation units

A compilation-unit defines the overall structure of a source file. A compilation unit consists of zero or more using-directives followed by zero or more namespace-member-declarations.

compilation-unit:
using-directivesopt namespace-member-declarationsopt

A C# program consists of one or more compilation units, each contained in a separate source file. When a C# program is compiled, all of the compilation units are processed together. Thus, compilation units can depend on each other, possibly in a circular fashion.

The using-directives of a compilation unit affect the namespace-member-declarations of that compilation unit, but have no effect on other compilation units.

The namespace-member-declarations of each compilation unit of a program contribute members to a single declaration space called the global namespace. For example:

File A.cs:

class A {}

File B.cs:

class B {}

The two compilation units contribute to the single global namespace, in this case declaring two classes with the fully qualified names A and B. Because the two compilation units contribute to the same declaration space, it would have been an error if each contained a declaration of a member with the same name.


9.2 Namespace declarations

A namespace-declaration consists of the keyword namespace, followed by a namespace name and body, optionally followed by a semicolon.

namespace-declaration:
namespace qualified-identifier namespace-body ;opt
qualified-identifier:
identifier
qualified-identifier . identifier
namespace-body:
{ using-directivesopt namespace-member-declarationsopt }

A namespace-declaration may occur as a top-level declaration in a compilation-unit or as a member declaration within another namespace-declaration. When a namespace-declaration occurs as a top-level declaration in a compilation-unit, the namespace becomes a member of the global namespace. When a namespace-declaration occurs within another namespace-declaration, the inner namespace becomes a member of the outer namespace. In either case, the name of a namespace must be unique within the containing namespace.

Namespaces are implicitly public and the declaration of a namespace cannot include any access modifiers.

Within a namespace-body, the optional using-directives import the names of other namespaces and types, allowing them to be referenced directly instead of through qualified names. The optional namespace-member-declarations contribute members to the declaration space of the namespace. Note that all using-directives must appear before any member declarations.

The qualified-identifier of a namespace-declaration may be single identifier or a sequence of identifiers separated by “.” tokens. The latter form permits a program to define a nested namespace without lexically nesting several namespace declarations. For example,

namespace N1.N2
{
   class A {}
   class B {}
}

is semantically equivalent to

namespace N1
{
   namespace N2
   {
      class A {}
      class B {}
   }
}

Namespaces are open-ended, and two namespace declarations with the same fully qualified name contribute to the same declaration space (§3.1). In the example

namespace N1.N2
{
   class A {}
}
namespace N1.N2
{
   class B {}
}

the two namespace declarations above contribute to the same declaration space, in this case declaring two classes with the fully qualified names N1.N2.A and N1.N2.B. Because the two declarations contribute to the same declaration space, it would have been an error if each contained a declaration of a member with the same name.


9.3 Using directives

Using directives facilitate the use of namespaces and types defined in other namespaces. Using directives impact the name resolution process of namespace-or-type-names (§3.6) and simple-names (§7.5.2), but unlike declarations, using directives do not contribute new members to the underlying declaration spaces of the compilation units or namespaces within which they are used.

using-directives:
using-directive
using-directives using-directive
using-directive:
using-alias-directive
using-namespace-directive

A using-alias-directive (§9.3.1) introduces an alias for a namespace or type.

A using-namespace-directive (§9.3.2) imports the type members of a namespace.

The scope of a using-directive extends over the namespace-member-declarations of its immediately containing compilation unit or namespace body. The scope of a using-directive specifically does not include its peer using-directives. Thus, peer using-directives do not affect each other, and the order in which they are written is insignificant.


9.3.1 Using alias directives

A using-alias-directive introduces an identifier that serves as an alias for a namespace or type within the immediately enclosing compilation unit or namespace body.

using-alias-directive:
using identifier = namespace-or-type-name ;

Within member declarations in a compilation unit or namespace body that contains a using-alias-directive, the identifier introduced by the using-alias-directive can be used to reference the given namespace or type. For example:

namespace N1.N2
{
   class A {}
}
namespace N3
{
   using A = N1.N2.A;
   class B: A {}
}

Here, within member declarations in the N3 namespace, A is an alias for N1.N2.A, and thus class N3.B derives from class N1.N2.A. The same effect can be obtained by creating an alias R for N1.N2 and then referencing R.A:

namespace N3
{
   using R = N1.N2;
   class B: R.A {}
}

The identifier of a using-alias-directive must be unique within the declaration space of the compilation unit or namespace that immediately contains the using-alias-directive. For example:

namespace N3
{
   class A {}
}
namespace N3
{
   using A = N1.N2.A;      // Error, A already exists
}

Here, N3 already contains a member A, so it is an error for a using-alias-directive to use that identifier. It is likewise an error for two or more using-alias-directives in the same compilation unit or namespace body to declare aliases by the same name.

A using-alias-directive makes an alias available within a particular compilation unit or namespace body, but it does not contribute any new members to the underlying declaration space. In other words, a using-alias-directive is not transitive but rather affects only the compilation unit or namespace body in which it occurs. In the example

namespace N3
{
   using R = N1.N2;
}
namespace N3
{
   class B: R.A {}         // Error, R unknown
}

the scope of the using-alias-directive that introduces R only extends to member declarations in the namespace body in which it is contained, and R is thus unknown in the second namespace declaration. However, placing the using-alias-directive in the containing compilation unit causes the alias to become available within both namespace declarations:

using R = N1.N2;
namespace N3
{
   class B: R.A {}
}
namespace N3
{
   class C: R.A {}
}

Just like regular members, names introduced by using-alias-directives are hidden by similarly named members in nested scopes. In the example

using R = N1.N2;
namespace N3
{
   class R {}
   class B: R.A {}      // Error, R has no member A
}

the reference to R.A in the declaration of B causes an error because R refers to N3.R, not N1.N2.

The order in which using-alias-directives are written has no significance, and resolution of the namespace-or-type-name referenced by a using-alias-directive is neither affected by the using-alias-directive itself nor by other using-directives in the immediately containing compilation unit or namespace body. In other words, the namespace-or-type-name of a using-alias-directive is resolved as if the immediately containing compilation unit or namespace body had no using-directives. In the example

namespace N1.N2 {}
namespace N3
{
   using R1 = N1;         // OK
   using R2 = N1.N2;      // OK
   using R3 = R1.N2;      // Error, R1 unknown
}

the last using-alias-directive is in error because it is not affected by the first using-alias-directive.

A using-alias-directive can create an alias for any namespace or type, including the namespace within which it appears and any namespace or type nested within that namespace.

Accessing a namespace or type through an alias yields exactly the same result as accessing the namespace or type through its declared name. In other words, given

namespace N1.N2
{
   class A {}
}
namespace N3
{
   using R1 = N1;
   using R2 = N1.N2;
   class B
   {
      N1.N2.A a;         // refers to N1.N2.A
      R1.N2.A b;         // refers to N1.N2.A
      R2.A c;            // refers to N1.N2.A
   }
}

the names N1.N2.A, R1.N2.A, and R2.A are completely equivalent and all refer to the class whose fully qualified name is N1.N2.A.


9.3.2 Using namespace directives

A using-namespace-directive imports the types contained in a namespace into the immediately enclosing compilation unit or namespace body, enabling the identifier of each type to be used without qualification.

using-namespace-directive:
using namespace-name ;

Within member declarations in compilation unit or namespace body that contains a using-namespace-directive, the types contained in the given namespace can be referenced directly. For example:

namespace N1.N2
{
   class A {}
}
namespace N3
{
   using N1.N2;
   class B: A {}
}

Here, within member declarations in the N3 namespace, the type members of N1.N2 are directly available, and thus class N3.B derives from class N1.N2.A.

A using-namespace-directive imports the types contained in the given namespace, but specifically does not import nested namespaces. In the example

namespace N1.N2
{
   class A {}
}
namespace N3
{
   using N1;
   class B: N2.A {}      // Error, N2 unknown
}

the using-namespace-directive imports the types contained in N1, but not the namespaces nested in N1. Thus, the reference to N2.A in the declaration of B is in error because no members named N2 are in scope.

Unlike a using-alias-directive, a using-namespace-directive may import types whose identifiers are already defined within the enclosing compilation unit or namespace body. In effect, names imported by a using-namespace-directive are hidden by similarly named members in the enclosing compilation unit or namespace body. For example:

namespace N1.N2
{
   class A {}
   class B {}
}
namespace N3
{
   using N1.N2;
   class A {}
}

Here, within member declarations in the N3 namespace, A refers to N3.A rather than N1.N2.A.

When more than one namespace imported by using-namespace-directives in the same compilation unit or namespace body contain types by the same name, references to that name are considered ambiguous. In the example

namespace N1
{
   class A {}
}
namespace N2
{
   class A {}
}
namespace N3
{
   using N1;
   using N2;
   class B: A {}            // Error, A is ambiguous
}

both N1 and N2 contain a member A, and because N3 imports both, referencing A in N3 is an error. In this situation, the conflict can be resolved either through qualification of references to A, or by introducing a using-alias-directive that picks a particular A. For example:

namespace N3
{
   using N1;
   using N2;
   using A = N1.A;
   class B: A {}            // A means N1.A
}

Like a using-alias-directive, a using-namespace-directive does not contribute any new members to the underlying declaration space of the compilation unit or namespace, but rather affects only the compilation unit or namespace body in which it appears.

The namespace-name referenced by a using-namespace-directive is resolved in the same way as the namespace-or-type-name referenced by a using-alias-directive. Thus, using-namespace-directives in the same compilation unit or namespace body do not affect each other and can be written in any order.


9.4 Namespace members

A namespace-member-declaration is either a namespace-declaration (§9.2) or a type-declaration (§9.5).

namespace-member-declarations:
namespace-member-declaration
namespace-member-declarations namespace-member-declaration
namespace-member-declaration:
namespace-declaration
type-declaration

A compilation unit or a namespace body can contain namespace-member-declarations, and such declarations contribute new members to the underlying declaration space of the containing compilation unit or namespace body.


9.5 Type declarations

A type-declaration is either a class-declaration (§10.1), a struct-declaration (§11), an interface-declaration (§13.1), an enum-declaration (§14.1), or a delegate-declaration (§15.1).

type-declaration:
class-declaration
struct-declaration
interface-declaration
enum-declaration
delegate-declaration

A type-declaration can occur as a top-level declaration in a compilation unit or as a member declaration within a namespace, class, or struct.

When a type declaration for a type T occurs as a top-level declaration in a compilation unit, the fully qualified name of the newly declared type is simply T. When a type declaration for a type T occurs within a namespace, class, or struct, the fully qualified name of the newly declared type is N.T, where N is the fully qualified name of the containing namespace, class, or struct.

A type declared within a class or struct is called a nested type (§10.2.6).

The permitted access modifiers and the default access for a type declaration depend on the context in which the declaration takes place (§3.3.1):

  • Types declared in compilation units or namespaces can have public or internal access. The default is internal access.
  • Types declared in classes can have public, protected internal, protected, internal, or private access. The default is private access.
  • Types declared in structs can have public, internal, or private access. The default is private access.


10. Classes

A class is a data structure that may contain data members (constants, fields, and events), function members (methods, properties, indexers, operators, constructors, and destructors), and nested types. Class types support inheritance, a mechanism whereby a derived class can extend and specialize a base class.


10.1 Class declarations

A class-declaration is a type-declaration (§9.5) that declares a new class.

class-declaration:
attributesopt class-modifiersopt class identifier class-baseopt class-body ;opt

A class-declaration consists of an optional set of attributes (§17), followed by an optional set of class-modifiers (§10.1.1), followed by the keyword class and an identifier that names the class, followed by an optional class-base specification (§10.1.2), followed by a class-body (§10.1.3), optionally followed by a semicolon.


10.1.1 Class modifiers

A class-declaration may optionally include a sequence of class modifiers:

class-modifiers:
class-modifier
class-modifiers class-modifier
class-modifier:
new
public
protected
internal
private
abstract
sealed

It is an error for the same modifier to appear multiple times in a class declaration.

The new modifier is only permitted on nested classes. It specifies that the class hides an inherited member by the same name, as described in §10.2.2.

The public, protected, internal, and private modifiers control the accessibility of the class. Depending on the context in which the class declaration occurs, some of these modifiers may not be permitted (§3.3.1).

The abstract and sealed modifiers are discussed in the following sections.


10.1.1.1 Abstract classes

The abstract modifier is used to indicate that a class is incomplete and intended only to be a base class of other classes. An abstract class differs from a non-abstract class is the following ways:

  • An abstract class cannot be instantiated directly, and it is an error to use the new operator on an abstract class. While it is possible to have variables and values whose compile-time types are abstract, such variables and values will necessarily either be null or contain references to instances of non-abstract classes derived from the abstract types.
  • An abstract class is permitted (but not required) to contain abstract members.
  • An abstract class cannot be sealed.

When a non-abstract class is derived from an abstract class, the non-abstract class must include actual implementations of all inherited abstract members. Such implementations are provided by overriding the abstract members. In the example

abstract class A
{
   public abstract void F();
}
abstract class B: A
{
   public void G() {}
}
class C: B
{
   public override void F() {
      // actual implementation of F
   }
}

the abstract class A introduces an abstract method F. Class B introduces an additional method G, but doesn’t provide an implementation of F. B must therefore also be declared abstract. Class C overrides F and provides an actual implementation. Since there are no outstanding abstract members in C, C is permitted (but not required) to be non-abstract.


10.1.1.2 Sealed classes

The sealed modifier is used to prevent derivation from a class. An error occurs if a sealed class is specified as the base class of another class.

A sealed class cannot also be an abstract class.

The sealed modifier is primarily used to prevent unintended derivation, but it also enables certain run-time optimizations. In particular, because a sealed class is known to never have any derived classes, it is possible to transform virtual function member invocations on sealed class instances into non-virtual invocations.


10.1.2 Class base specification

A class declaration may include a class-base specification which defines the direct base class of the class and the interfaces implemented by the class.

class-base:
: class-type
: interface-type-list
: class-type , interface-type-list
interface-type-list:
interface-type
interface-type-list , interface-type


10.1.2.1 Base classes

When a class-type is included in the class-base, it specifies the direct base class of the class being declared. If a class declaration has no class-base, or if the class-base lists only interface types, the direct base class is assumed to be object. A class inherits members from its direct base class, as described in §10.2.1.

In the example

class A {}
class B: A {}

class A is said to be the direct base class of B, and B is said to be derived from A. Since A does not explicitly specify a direct base class, its direct base class is implicitly object.

The direct base class of a class type must be at least as accessible as the class type itself (§3.3.4). For example, it is an error for a public class to derive from a private or internal class.

The direct base class of a class type must not be any of the following types: System.Array, System.Delegate, System.Enum, or System.ValueType.

The base classes of a class are the direct base class and its base classes. In other words, the set of base classes is the transitive closure of the direct base class relationship. Referring to the example above, the base classes of B are A and object.

Except for class object, every class has exactly one direct base class. The object class has no direct base class and is the ultimate base class of all other classes.

When a class B derives from a class A, it is an error for A to depend on B. A class directly depends on its direct base class (if any) and directly depends on the class within which it is immediately nested (if any). Given this definition, the complete set of classes upon which a class depends is the transitive closure of the directly depends on relationship.

The example

class A: B {}
class B: C {}
class C: A {}

is in error because the classes circularly depend on themselves. Likewise, the example

class A: B.C {}
class B: A
{
   public class C {}
}

is in error because A depends on B.C (its direct base class), which depends on B (its immediately enclosing class), which circularly depends on A.

Note that a class does not depend on the classes that are nested within it. In the example

class A
{
   class B: A {}
}

B depends on A (because A is both its direct base class and its immediately enclosing class), but A does not depend on B (since B is neither a base class nor an enclosing class of A). Thus, the example is valid.

It is not possible to derive from a sealed class. In the example

sealed class A {}
class B: A {}         // Error, cannot derive from a sealed class

class B is in error because it attempts to derive from the sealed class A.


10.1.2.2 Interface implementations

A class-base specification may include a list of interface types, in which case the class is said to implement the given interface types. Interface implementations are discussed further in §13.4.


10.1.3 Class body

The class-body of a class defines the members of the class.

class-body:
{ class-member-declarationsopt }


10.2 Class members

The members of a class consist of the members introduced by its class-member-declarations and the members inherited from the direct base class.

class-member-declarations:
class-member-declaration
class-member-declarations class-member-declaration
class-member-declaration:
constant-declaration
field-declaration
method-declaration
property-declaration
event-declaration
indexer-declaration
operator-declaration
constructor-declaration
destructor-declaration
static-constructor-declaration
type-declaration

The members of a class are divided into the following categories:

  • Constants, which represent constant values associated with the class (§10.3).
  • Fields, which are the variables of the class (§10.4).
  • Methods, which implement the computations and actions that can be performed by the class (§10.5).
  • Properties, which define named attributes and the actions associated with reading and writing those attributes (§10.6).
  • Events, which define notifications that are generated by the class (§10.7).
  • Indexers, which permit instances of the class to be indexed in the same way as arrays (§10.8).
  • Operators, which define the expression operators that can be applied to instances of the class (§10.9).
  • Instance constructors, which implement the actions required to initialize instances of the class (§10.10)
  • Destructors, which implement the actions to perform before instances of the class are permanently discarded (§10.11).
  • Static constructors, which implement the actions required to initialize the class itself (§10.12).
  • Types, which represent the types that are local to the class (§9.5).

Members that contain executable code are collectively known as the function members of the class. The function members of a class are the methods, properties, indexers, operators, constructors, and destructors of the class.

A class-declaration creates a new declaration space (§3.1), and the class-member-declarations immediately contained by the class-declaration introduce new members into this declaration space. The following rules apply to class-member-declarations:

  • Constructors and destructors must have the same name as the immediately enclosing class. All other members must have names that differ from the name of the immediately enclosing class.
  • The name of a constant, field, property, event, or type must differ from the names of all other members declared in the same class.
  • The name of a method must differ from the names of all other non-methods declared in the same class. In addition, the signature (§3.4) of a method must differ from the signatures of all other methods declared in the same class.
  • The signature of an constructor must differ from the signatures of all other constructors declared in the same class.
  • The signature of an indexer must differ from the signatures of all other indexers declared in the same class.
  • The signature of an operator must differ from the signatures of all other operators declared in the same class.

The inherited members of a class (§10.2.1) are specifically not part of the declaration space of a class. Thus, a derived class is allowed to declare a member with the same name or signature as an inherited member (which in effect hides the inherited member).


10.2.1 Inheritance

A class inherits the members of its direct base class. Inheritance means that a class implicitly contains all members of its direct base class, except for the constructors and destructors of the base class. Some important aspects of inheritance are:

  • Inheritance is transitive. If C is derived from B, and B is derived from A, then C inherits the members declared in B as well as the members declared in A.
  • A derived class extends its direct base class. A derived class can add new members to those it inherits, but it cannot remove the definition of an inherited member.
  • Constructors and destructors are not inherited, but all other members are, regardless of their declared accessibility (§3.3). However, depending on their declared accessibility, inherited members may not be accessible in a derived class.
  • A derived class can hide (§3.5.1.2) inherited members by declaring new members with the same name or signature. Note however that hiding an inherited member does not remove the member—it merely makes the member inaccessible in the derived class.
  • An instance of a class contains a copy of all instance fields declared in the class and its base classes, and an implicit conversion (§6.1.4) exists from a derived class type to any of its base class types. Thus, a reference to a derived class instance can be treated as a reference to a base class instance.
  • A class can declare virtual methods, properties, and indexers, and derived classes can override the implementation of these function members. This enables classes to exhibit polymorphic behavior wherein the actions performed by a function member invocation varies depending on the run-time type of the instance through which the function member is invoked.


10.2.2 The new modifier

A class-member-declaration is permitted to declare a member with the same name or signature as an inherited member. When this occurs, the derived class member is said to hide the base class member. Hiding an inherited member is not considered an error, but it does cause the compiler to issue a warning. To suppress the warning, the declaration of the derived class member can include a new modifier to indicate that the derived member is intended to hide the base member. This topic is discussed further in §3.5.1.2.

If a new modifier is included in a declaration that doesn’t hide an inherited member, a warning is issued to that effect. This warning is suppressed by removing the new modifier.

It is an error to use the new and override modifiers in the same declaration.


10.2.3 Access modifiers

A class-member-declaration can have any one of the five possible types of declared accessibility (§3.3.1): public, protected internal, protected, internal, or private. Except for the protected internal combination, it is an error to specify more than one access modifier. When a class-member-declaration does not include any access modifiers, the declaration defaults to private declared accessibility.


10.2.4 Constituent types

Types that are referenced in the declaration of a member are called the constituent types of the member. Possible constituent types are the type of a constant, field, property, event, or indexer, the return type of a method or operator, and the parameter types of a method, indexer, operator, or constructor. The constituent types of a member must be at least as accessible as the member itself (§3.3.4).


10.2.5 Static and instance members

Members of a class are either static members or instance members. Generally speaking, it is useful to think of static members as belonging to classes and instance members as belonging to objects (instances of classes).

When a field, method, property, event, operator, or constructor declaration includes a static modifier, it declares a static member. In addition, a constant or type declaration implicitly declares a static member. Static members have the following characteristics:

  • When a static member is referenced in a member-access (§7.5.4) of the form E.M, E must denote a type. It is an error for E to denote an instance.
  • A static field identifies exactly one storage location. No matter how many instances of a class are created, there is only ever one copy of a static field.
  • A static function member (method, property, indexer, operator, or constructor) does not operate on a specific instance, and it is an error to refer to this in a static function member.

When a field, method, property, event, indexer, constructor, or destructor declaration does not include a static modifier, it declares an instance member. An instance member is sometimes called a non-static member. Instance members have the following characteristics:

  • When an instance member is referenced in a member-access (§7.5.4) of the form E.M, E must denote an instance. It is an error for E to denote a type.
  • Every instance of a class contains a separate copy of all instance fields of the class.
  • An instance function member (method, property accessor, indexer accessor, constructor, or destructor) operates on a given instance of the class, and this instance can be accessed as this (§7.5.7).

The following example illustrates the rules for accessing static and instance members:

class Test
{
   int x;
   static int y;
   void F() {
      x = 1;         // Ok, same as this.x = 1
      y = 1;         // Ok, same as Test.y = 1
   }
   static void G() {
      x = 1;         // Error, cannot access this.x
      y = 1;         // Ok, same as Test.y = 1
   }
   static void Main() {
      Test t = new Test();
      t.x = 1;         // Ok
      t.y = 1;         // Error, cannot access static member through instance
      Test.x = 1;      // Error, cannot access instance member through type
      Test.y = 1;      // Ok
   }
}

The F method shows that in an instance function member, a simple-name (§7.5.2) can be used to access both instance members and static members. The G method shows that in a static function member, it is an error to access an instance member through a simple-name. The Main method shows that in a member-access (§7.5.4), instance members must be accessed through instances, and static members must be accessed through types.


10.2.6 Nested types

© 1987-2000 Microsoft Corporation. All rights reserved.


10.3 Constants

A constant is a class member that represents a constant value: a value that can be computed at compile-time. A constant-declaration introduces one or more constants of a given type.

constant-declaration:
attributesopt constant-modifiersopt const type constant-declarators ;
constant-modifiers:
constant-modifier
constant-modifiers constant-modifier
constant-modifier:
new
public
protected
internal
private
constant-declarators:
constant-declarator
constant-declarators , constant-declarator
constant-declarator:
identifier = constant-expression

A constant-declaration may include a set of attributes (§17), a new modifier (§10.2.2), and a valid combination of the four access modifiers (§10.2.3). The attributes and modifiers apply to all of the members declared by the constant-declaration. Even though constants are considered static members, a constant-declaration neither requires nor allows a static modifier.

The type of a constant-declaration specifies the type of the members introduced by the declaration. The type is followed by a list of constant-declarators, each of which introduces a new member. A constant-declarator consists of an identifier that names the member, followed by an “=” token, followed by a constant-expression (§7.15) that gives the value of the member.

The type specified in a constant declaration must be sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, string, an enum-type, or a reference-type. Each constant-expression must yield a value of the target type or of a type that can be converted to the target type by an implicit conversion (§6.1).

The type of a constant must be at least as accessible as the constant itself (§3.3.4).

A constant can itself participate in a constant-expression. Thus, a constant may be used in any construct that requires a constant-expression. Examples of such constructs include case labels, goto case statements, enum member declarations, attributes, and other constant declarations.

As described in §7.15, a constant-expression is an expression that can be fully evaluated at compile-time. Since the only way to create a non-null value of a reference-type other than string is to apply the new operator, and since the new operator is not permitted in a constant-expression, the only possible value for constants of reference-types other than string is null.

When a symbolic name for a constant value is desired, but when the type of the value is not permitted in a constant declaration or when the value cannot be computed at compile-time by a constant-expression, a readonly field (§10.4.2) may be used instead.

A constant declaration that declares multiple constants is equivalent to multiple declarations of single constants with the same attributes, modifiers, and type. For example

class A
{
   public const double X = 1.0, Y = 2.0, Z = 3.0;
}

is equivalent to

class A
{
   public const double X = 1.0;
   public const double Y = 2.0;
   public const double Z = 3.0;
}

Constants are permitted to depend on other constants within the same program as long as the dependencies are not of a circular nature. The compiler automatically arranges to evaluate the constant declarations in the appropriate order. In the example

class A
{
   public const int X = B.Z + 1;
   public const int Y = 10;
}
class B
{
   public const int Z = A.Y + 1;
}

the compiler first evaluates Y, then evaluates Z, and finally evaluates X, producing the values 10, 11, and 12. Constant declarations may depend on constants from other programs, but such dependencies are only possible in one direction. Referring to the example above, if A and B were declared in separate programs, it would be possible for A.X to depend on B.Z, but B.Z could then not simultaneously depend on A.Y.


10.4 Fields

A field is a member that represents a variable associated with an object or class. A field-declaration introduces one or more fields of a given type.

field-declaration:
attributesopt field-modifiersopt type variable-declarators ;
field-modifiers:
field-modifier
field-modifiers field-modifier
field-modifier:
new
public
protected
internal
private
static
readonly
variable-declarators:
variable-declarator
variable-declarators , variable-declarator
variable-declarator:
identifier
identifier = variable-initializer
variable-initializer:
expression
array-initializer

A field-declaration may include a set of attributes (§17), a new modifier (§10.2.2), a valid combination of the four access modifiers (§10.2.3), a static modifier (§10.4.1), and a readonly modifier (§10.4.2). The attributes and modifiers apply to all of the members declared by the field-declaration.

The type of a field-declaration specifies the type of the members introduced by the declaration. The type is followed by a list of variable-declarators, each of which introduces a new member. A variable-declarator consists of an identifier that names the member, optionally followed by an “=” token and a variable-initializer (§10.4.4) that gives the initial value of the member.

The type of a field must be at least as accessible as the field itself (§3.3.4).

The value of a field is obtained in an expression using a simple-name (§7.5.2) or a member-access (§7.5.4). The value of a field is modified using an assignment (§7.13). The value of a field can be both obtained and modified using postfix increment and decrement operators (§7.5.9) and prefix increment and decrement operators (§7.6.7).

A field declaration that declares multiple fields is equivalent to multiple declarations of single fields with the same attributes, modifiers, and type. For example

class A
{
   public static int X = 1, Y, Z = 100;
}

is equivalent to

class A
{
   public static int X = 1;
   public static int Y;
   public static int Z = 100;
}


10.4.1 Static and instance fields

When a field-declaration includes a static modifier, the fields introduced by the declaration are static fields. When no static modifier is present, the fields introduced by the declaration are instance fields. Static fields and instance fields are two of the several kinds of variables (§5) supported by C#, and are at times referred to as static variables and instance variables.

A static field identifies exactly one storage location. No matter how many instances of a class are created, there is only ever one copy of a static field. A static field comes into existence when the type in which it is declared is loaded, and ceases to exist when the type in which it is declared is unloaded.

Every instance of a class contains a separate copy of all instance fields of the class. An instance field comes into existence when a new instance of its class is created, and ceases to exist when there are no references to that instance and the destructor of the instance has executed.

When a field is referenced in a member-access (§7.5.4) of the form E.M, if M is a static field, E must denote a type, and if M is an instance field, E must denote an instance.

The differences between static and instance members are further discussed in §10.2.5.


10.4.2 Readonly fields

When a field-declaration includes a readonly modifier, assignments to the fields introduced by the declaration can only occur as part of the declaration or in a constructor in the same class. Specifically, assignments to a readonly field are permitted only in the following contexts:

  • In the variable-declarator that introduces the field (by including a variable-initializer in the declaration).
  • For an instance field, in the instance constructors of the class that contains the field declaration, or for a static field, in the static constructor of the class the that contains the field declaration. These are also the only contexts in which it is valid to pass a readonly field as an out or ref parameter.

Attempting to assign to a readonly field or pass it as an out or ref parameter in any other context is an error.


10.4.2.1 Using static readonly fields for constants

A static readonly field is useful when a symbolic name for a constant value is desired, but when the type of the value is not permitted in a const declaration or when the value cannot be computed at compile-time. In the example

public class Color
{
   public static readonly Color Black = new Color(0, 0, 0);
   public static readonly Color White = new Color(255, 255, 255);
   public static readonly Color Red = new Color(255, 0, 0);
   public static readonly Color Green = new Color(0, 255, 0);
   public static readonly Color Blue = new Color(0, 0, 255);
   private byte red, green, blue;
   public Color(byte r, byte g, byte b) {
      red = r;
      green = g;
      blue = b;
   }
}

the Black, White, Red, Green, and Blue members cannot be declared as const members because their values cannot be computed at compile-time. However, declaring the members as static readonly fields has much the same effect.


10.4.2.2 Versioning of constants and static readonly fields

Constants and readonly fields have different binary versioning semantics. When an expression references a constant, the value of the constant is obtained at compile-time, but when an expression references a readonly field, the value of the field is not obtained until run-time. Consider an application that consists of two separate programs:

namespace Program1
{
   public class Utils
   {
      public static readonly int X = 1;
   }
}
namespace Program2
{
   class Test
   {
      static void Main() {
         Console.WriteLine(Program1.Utils.X);
      }
   }
}

The Program1 and Program2 namespaces denote two programs that are compiled separately. Because Program1.Utils.X is declared as a static readonly field, the value output by the Console.WriteLine statement is not known at compile-time, but rather is obtained at run-time. Thus, if the value of X is changed and Program1 is recompiled, the Console.WriteLine statement will output the new value even if Program2 isn’t recompiled. However, had X been a constant, the value of X would have been obtained at the time Program2 was compiled, and would remain unaffected by changes in Program1 until Program2 is recompiled.


10.4.3 Field initialization

The initial value of a field is the default value (§5.2) of the field’s type. When a class is loaded, all static fields are initialized to their default values, and when an instance of a class is created, all instance fields are initialized to their default values. It is not possible to observe the value of a field before this default initialization has occurred, and a field is thus never “uninitialized”. The example

class Test
{
   static bool b;
   int i;
   static void Main() {
      Test t = new Test();
      Console.WriteLine("b = {0}, i = {1}", b, t.i);
   }
}

produces the output

b = False, i = 0

because b is automatically initialized to its default value when the class is loaded and i is automatically initialized to its default value when an instance of the class is created.


10.4.4 Variable initializers

Field declarations may include variable-initializers. For static fields, variable initializers correspond to assignment statements that are executed when the class is loaded. For instance fields, variable initializers correspond to assignment statements that are executed when an instance of the class is created.

The example

class Test
{
   static double x = Math.Sqrt(2.0);
   int i = 100;
   string s = "Hello";
   static void Main() {
      Test a = new Test();
      Console.WriteLine("x = {0}, i = {1}, s = {2}", x, a.i, a.s);
   }
}

produces the output

x = 1.414213562373095, i = 100, s = Hello

because an assignment to x occurs when the class is loaded and assignments to i and s occur when an new instance of the class is created.

The default value initialization described in §10.4.3 occurs for all fields, including fields that have variable initializers. Thus, when a class is loaded, all static fields are first initialized to their default values, and then the static field initializers are executed in textual order. Likewise, when an instance of a class is created, all instance fields are first initialized to their default values, and then the instance field initializers are executed in textual order.

It is possible for static fields with variable initializers to be observed in their default value state, though this is strongly discouraged as a matter of style. The example

class Test
{
   static int a = b + 1;
   static int b = a + 1;
   static void Main() {
      Console.WriteLine("a = {0}, b = {1}", a, b);
   }
}

exhibits this behavior. Despite the circular definitions of a and b, the program is legal. It produces the output

a = 1, b = 2

because the static fields a and b are initialized to 0 (the default value for int) before their initializers are executed. When the initializer for a runs, the value of b is zero, and so a is initialized to 1. When the initializer for b runs, the value of a is already 1, and so b is initialized to 2.


10.4.4.1 Static field initialization

The static field variable initializers of a class correspond to a sequence of assignments that are executed immediately upon entry to the static constructor of the class. The variable initializers are executed in the textual order they appear in the class declaration. The class loading and initialization process is described further in §10.12.


10.4.4.2 Instance field initialization

The instance field variable initializers of a class correspond to a sequence of assignments that are executed immediately upon entry to one of the instance constructors of the class. The variable initializers are executed in the textual order they appear in the class declaration. The class instance creation and initialization process is described further in §10.10.

A variable initializer for an instance field cannot reference the instance being created. Thus, it is an error to reference this in a variable initializer, as is it an error for a variable initializer to reference any instance member through a simple-name. In the example

class A
{
   int x = 1;
   int y = x + 1;      // Error, reference to instance member of this
}

the variable initializer for y is in error because it references a member of the instance being created.


10.5 Methods

A method is a member that implements a computation or action that can be performed by an object or class. Methods are declared using method-declarations:

method-declaration:
method-header method-body
method-header:
attributesopt method-modifiersopt return-type member-name ( formal-parameter-listopt )
method-modifiers:
method-modifier
method-modifiers method-modifier
method-modifier:
new
public
protected
internal
private
static
virtual
override
abstract
extern
return-type:
type
void
member-name:
identifier
interface-type . identifier
method-body:
block
;

A method-declaration may include a set of attributes (§17), a new modifier (§10.2.2), a valid combination of the four access modifiers (§10.2.3), one of the static (§10.5.2), virtual (§10.5.3), override (§10.5.4), or abstract (§10.5.5) modifiers, and an extern (§10.5.6) modifier.

The return-type of a method declaration specifies the type of the value computed and returned by the method. The return-type is void if the method does not return a value.

The member-name specifies the name of the method. Unless the method is an explicit interface member implementation, the member-name is simply an identifier. For an explicit interface member implementation (§13.4.1) , the member-name consists of an interface-type followed by a “.” and an identifier.

The optional formal-parameter-list specifies the parameters of the method (§10.5.1).

The return-type and each of the types referenced in the formal-parameter-list of a method must be at least as accessible as the method itself (§3.3.4).

For abstract and extern methods, the method-body consists simply of a semicolon. For all other methods, the method-body consists of a block which specifies the statements to execute when the method is invoked.

The name and the formal parameter list of a method defines the signature (§3.4) of the method. Specifically, the signature of a method consists of its name and the number, modifiers, and types of its formal parameters. The return type is not part of a method’s signature, nor are the names of the formal parameters.

The name of a method must differ from the names of all other non-methods declared in the same class. In addition, the signature of a method must differ from the signatures of all other methods declared in the same class.


10.5.1 Method parameters

The parameters of a method, if any, are declared by the method’s formal-parameter-list.

formal-parameter-list:
fixed-parameters
fixed-parameters , parameter-array
parameter-array
fixed-parameters:
fixed-parameter
fixed-parameters , fixed-parameter
fixed-parameter:
attributesopt parameter-modifieropt type identifier
parameter-modifier:
ref
out
parameter-array:
attributesopt params array-type identifier

The formal parameter list consists of one or more fixed-parameters optionally followed by a single parameter-array, all separated by commas.

A fixed-parameter consists of an optional set of attributes (§17), an optional ref or out modifier, a type, and an identifier. Each fixed-parameter declares a parameter of the given type with the given name.

A parameter-array consists of an optional set of attributes (§17), a params modifier, an array-type, and an identifier. A parameter array declares a single parameter of the given array type with the given name. The array-type of a parameter array must be a single-dimensional array type (§12.1). In a method invocation, a parameter array permits either a single argument of the given array type to be specified, or it permits zero or more arguments of the array element type to be specified. Parameter arrays are further described in §10.5.1.4.

A method declaration creates a separate declaration space for parameters and local variables. Names are introduced into this declaration space by the formal parameter list of the method and by local variable declarations in the block of the method. All names in the declaration space of a method must be unique. Thus, it is an error for a parameter or local variable to have the same name as another parameter or local variable.

A method invocation (§7.5.5.1) creates a copy, specific to that invocation, of the formal parameters and local variables of the method, and the argument list of the invocation assigns values or variable references to the newly created formal parameters. Within the block of a method, formal parameters can be referenced by their identifiers in simple-name expressions (§7.5.2).

There are four kinds of formal parameters:

  • Value parameters, which are declared without any modifiers.
  • Reference parameters, which are declared with the ref modifier.
  • Output parameters, which are declared with the out modifier.
  • Parameter arrays, which are declared with the params modifier.

As described in §3.4, the ref and out modifiers are part of a method’s signature, but the params modifier is not.


10.5.1.1 Value parameters

A parameter declared with no modifiers is a value parameter. A value parameter corresponds to a local variable that gets its initial value from the corresponding argument supplied in the method invocation.

When a formal parameter is a value parameter, the corresponding argument in a method invocation must be an expression of a type that is implicitly convertible (§6.1) to the formal parameter type.

A method is permitted to assign new values to a value parameter. Such assignments only affect the local storage location represented by the value parameter—they have no effect on the actual argument given in the method invocation.


10.5.1.2 Reference parameters

A parameter declared with a ref modifier is a reference parameter. Unlike a value parameter, a reference parameter does not create a new storage location. Instead, a reference parameter represents the same storage location as the variable given as the argument in the method invocation.

When a formal parameter is a reference parameter, the corresponding argument in a method invocation must consist of the keyword ref followed by a variable-reference (§5.4) of the same type as the formal parameter. A variable must be definitely assigned before it can be passed as a reference parameter.

Within a method, a reference parameter is always considered definitely assigned.

The example

class Test
{
   static void Swap(ref int x, ref int y) {
      int temp = x;
      x = y;
      y = temp;
   }
   static void Main() {
      int i = 1, j = 2;
      Swap(ref i, ref j);
      Console.WriteLine("i = {0}, j = {1}", i, j);
   }
}

produces the output

i = 2, j = 1

For the invocation of Swap in Main, x represents i and y represents j. Thus, the invocation has the effect of swapping the values of i and j.

In a method that takes reference parameters it is possible for multiple names to represent the same storage location. In the example

class A
{
   string s;
   void F(ref string a, ref string b) {
      s = "One";
      a = "Two";
      b = "Three";
   }
   void G() {
      F(ref s, ref s);
   }
}

the invocation of F in G passes a reference to s for both a and b. Thus, for that invocation, the names s, a, and b all refer to the same storage location, and the three assignments all modify the instance field s.


10.5.1.3 Output parameters

A parameter declared with an out modifier is an output parameter. Similar to a reference parameter, an output parameter does not create a new storage location. Instead, an output parameter represents the same storage location as the variable given as the argument in the method invocation.

When a formal parameter is an output parameter, the corresponding argument in a method invocation must consist of the keyword out followed by a variable-reference (§5.4) of the same type as the formal parameter. A variable need not be definitely assigned before it can be passed as an output parameter, but following an invocation where a variable was passed as an output parameter, the variable is considered definitely assigned.

Within a method, just like a local variable, an output parameter is initially considered unassigned and must be definitely assigned before its value is used.

Every output parameter of a method must be definitely assigned before the method returns.

Output parameters are typically used in methods that produce multiple return values. For example:

class Test
{
   static void SplitPath(string path, out string dir, out string name) {
      int i = path.Length;
      while (i > 0) {
         char ch = path[i – 1];
         if (ch == '\\' || ch == '/' || ch == ':') break;
         i--;
      }
      dir = path.Substring(0, i);
      name = path.Substring(i);
   }
   static void Main() {
      string dir, name;
      SplitPath("c:\\Windows\\System\\hello.txt", out dir, out name);
      Console.WriteLine(dir);
      Console.WriteLine(name);
   }
}

The example produces the output:

c:\Windows\System\
hello.txt

Note that the dir and name variables can be unassigned before they are passed to SplitPath, and that they are considered definitely assigned following the call.


10.5.1.4 Params parameters

C# Language Reference

10.5.1.4 Parameter arrays

[This topic is part of a preview and is subject to change in future releases. Blank topics are included as placeholders.]

A parameter declared with a params modifier is a parameter array. If a formal parameter list includes a parameter array, it must be the last parameter in the list and it must be of a single-dimensional array type. For example, the types string[] and string[][] can be used as the type of a parameter array, but the type string[,] can not. It is not possible to combine the params modifier with the ref and out modifiers.

A parameter array permits arguments to be specified in one of two ways in a method invocation:

  • The argument given for a parameter array can be a single expression of a type that is implicitly convertible (§6.1) to the parameter array type. In this case, the parameter array acts precisely like a value parameter.
  • Alternatively, the invocation can specify zero or more arguments for the parameter array, where each argument is an expression of a type that is implicitly convertible (§6.1) to the element type of the parameter array. In this case, the invocation creates an instance of the parameter array type with a length corresponding to the number of arguments, initializes the elements of the array instance with the given argument values, and uses the newly created array instance as the actual argument.

Except for allowing a variable number of arguments in an invocation, a parameter array is precisely equivalent to a value parameter (§10.5.1.1) of the same type.

The example

class Test
{
   static void F(params int[] args) {
      Console.WriteLine("Array contains {0} elements:", args.Length);
      foreach (int i in args) Console.Write(" {0}", i);
      Console.WriteLine();
   }
   static void Main() {
      int[] a = {1, 2, 3};
      F(a);
      F(10, 20, 30, 40);
      F();
   }
}

produces the output

Array contains 3 elements: 1 2 3
Array contains 4 elements: 10 20 30 40
Array contains 0 elements:

The first invocation of F simply passes the array a as a value parameter. The second invocation of F automatically creates a four-element int[] with the given element values and passes that array instance as a value parameter. Likewise, the third invocation of F creates a zero-element int[] and passes that instance as a value parameter. The second and third invocations are precisely equivalent to writing:

F(new int[] {10, 20, 30, 40});
F(new int[] {});

When performing overload resolution, a method with a parameter array may be applicable either in its normal form or in its expanded form (§7.4.2.1). The expanded form of a method is available only if the normal form of the method is not applicable and only if a method with the same signature as the expanded form is not already declared in the same type.

The example

class Test
{
   static void F(params object[] a) {
      Console.WriteLine("F(object[])");
   }
   static void F() {
      Console.WriteLine("F()");
   }
   static void F(object a0, object a1) {
      Console.WriteLine("F(object,object)");
   }
   static void Main() {
      F();
      F(1);
      F(1, 2);
      F(1, 2, 3);
      F(1, 2, 3, 4);
   }
}

produces the output

F();
F(object[]);
F(object,object);
F(object[]);
F(object[]);

In the example, two of the possible expanded forms of the method with a parameter array are already included in the class as regular methods. These expanded forms are therefore not considered when performing overload resolution, and the first and third method invocations thus select the regular methods. When a class declares a method with a parameter array, it is not uncommon to also include some of the expanded forms as regular methods. By doing so it is possible to avoid the allocation of an array instance that occurs when an expanded form of a method with a parameter array is invoked.

When the type of a parameter array is object[], a potential ambiguity arises between the normal form of the method and the expended form for a single object parameter. The reason for the ambiguity is that an object[] is itself implicitly convertible to type object. The ambiguity presents no problem, however, since it can be resolved by inserting a cast if needed.

The example

class Test
{
   static void F(params object[] args) {
      foreach (object o in a) {
         Console.Write(o.GetType().FullName);
         Console.Write(" ");
      }
      Console.WriteLine();
   }
   static void Main() {
      object[] a = {1, "Hello", 123.456};
      object o = a;
      F(a);
      F((object)a);
      F(o);
      F((object[])o);
   }
}

produces the output

System.Int32 System.String System.Double
System.Object[]
System.Object[]
System.Int32 System.String System.Double

In the first and last invocations of F, the normal form of F is applicable because an implicit conversion exists from the argument type to the parameter type (both are of type object[]). Thus, overload resolution selects the normal form of F, and the argument is passed as a regular value parameter. In the second and third invocations, the normal form of F is not applicable because no implicit conversion exists from the argument type to the parameter type (type object cannot be implicitly converted to type object[]). However, the expanded form of F is applicable, and it is therefore selected by overload resolution. As a result, a one-element object[] is created by the invocation, and the single element of the array is initialized with the given argument value (which itself is a reference to an object[]).


10.5.2 Static and instance methods

When a method declaration includes a static modifier, the method is said to be a static method. When no static modifier is present, the method is said to be an instance method.

A static method does not operate on a specific instance, and it is an error to refer to this in a static method. It is furthermore an error to include a virtual, abstract, or override modifier on a static method.

An instance method operates on a given instance of a class, and this instance can be accessed as this (§7.5.7).

The differences between static and instance members are further discussed in §10.2.5.


10.5.3 Virtual methods

When an instance method declaration includes a virtual modifier, the method is said to be a virtual method. When no virtual modifier is present, the method is said to be a non-virtual method.

It is an error for a method declaration that includes the virtual modifier to also include any one of the static, abstract, or override modifiers.

The implementation of a non-virtual method is invariant: The implementation is the same whether the method is invoked on an instance of the class in which it is declared or an instance of a derived class. In contrast, the implementation of a virtual method can be changed by derived classes. The process of changing the implementation of an inherited virtual method is known as overriding the method (§10.5.4).

In a virtual method invocation, the run-time type of the instance for which the invocation takes place determines the actual method implementation to invoke. In a non-virtual method invocation, the compile-time type of the instance is the determining factor. In precise terms, when a method named N is invoked with an argument list A on an instance with a compile-time type C and a run-time type R (where R is either C or a class derived from C), the invocation is processed as follows:

  • First, overload resolution is applied to C, N, and A, to select a specific method M from the set of methods declared in and inherited by C. This is described in §7.5.5.1.
  • Then, if M is a non-virtual method, M is invoked.
  • Otherwise, M is a virtual method, and the most derived implementation of M with respect to R is invoked.

For every virtual method declared in or inherited by a class, there exists a most derived implementation of the method with respect to that class. The most derived implementation of a virtual method M with respect to a class R is determined as follows:

  • If R contains the introducing virtual declaration of M, then this is the most derived implementation of M.
  • Otherwise, if R contains an override of M, then this is the most derived implementation of M.
  • Otherwise, the most derived implementation of M is the same as that of the direct base class of R.

The following example illustrates the differences between virtual and non-virtual methods:

class A
{
   public void F() { Console.WriteLine("A.F"); }
   public virtual void G() { Console.WriteLine("A.G"); }
}
class B: A
{
   new public void F() { Console.WriteLine("B.F"); }
   public override void G() { Console.WriteLine("B.G"); }
}
class Test
{
   static void Main() {
      B b = new B();
      A a = b;
      a.F();
      b.F();
      a.G();
      b.G();
   }
}

In the example, A introduces a non-virtual method F and a virtual method G. The class B introduces a new non-virtual method F, thus hiding the inherited F, and also overrides the inherited method G. The example produces the output:

A.F
B.F
B.G
B.G

Notice that the statement a.G() invokes B.G, not A.G. This is because the run-time type of the instance (which is B), not the compile-time type of the instance (which is A), determines the actual method implementation to invoke.

Because methods are allowed to hide inherited methods, it is possible for a class to contain several virtual methods with the same signature. This does not present an ambiguity problem, since all but the most derived method are hidden. In the example

class A
{
   public virtual void F() { Console.WriteLine("A.F"); }
}
class B: A
{
   public override void F() { Console.WriteLine("B.F"); }
}
class C: B
{
   new public virtual void F() { Console.WriteLine("C.F"); }
}
class D: C
{
   public override void F() { Console.WriteLine("D.F"); }
}
class Test
{
   static void Main() {
      D d = new D();
      A a = d;
      B b = d;
      C c = d;
      a.F();
      b.F();
      c.F();
      d.F();
   }
}

the C and D classes contain two virtual methods with the same signature: The one introduced by A and the one introduced by C. The method introduced by C hides the method inherited from A. Thus, the override declaration in D overrides the method introduced by C, and it is not possible for D to override the method introduced by A. The example produces the output:

B.F
B.F
D.F
D.F

Note that it is possible to invoke the hidden virtual method by accessing an instance of D through a less derived type in which the method is not hidden.


10.5.4 Override methods

When an instance method declaration includes an override modifier, the method is said to be an override method. An override method overrides an inherited virtual method with the same signature. Whereas a virtual method declaration introduces a new method, an override method declaration specializes an existing inherited virtual method by providing a new implementation of the method.

It is an error for an override method declaration to include any one of the new, static, or virtual modifiers. An override method declaration may include the abstract modifier. This enables a virtual method to be overridden by an abstract method.

The method overridden by an override declaration is known as the overridden base method. For an override method M declared in a class C, the overridden base method is determined by examining each base class of C, starting with the direct base class of C and continuing with each successive direct base class, until an accessible method with the same signature as M is located. For purposes of locating the overridden base method, a method is considered accessible if it is public, if it is protected, if it is protected internal, or if it is internal and declared in the same program as C.

A compile-time error occurs unless all of the following are true for an override declaration:

  • An overridden base method can be located as described above.
  • The overridden base method is a virtual, abstract, or override method. In other words, the overridden base method cannot be static or non-virtual.
  • The override declaration and the overridden base method have the same declared accessibility. In other words, an override declaration cannot change the accessibility of the virtual method.

An override declaration can access the overridden base method using a base-access (§7.5.8). In the example

class A
{
   int x;
   public virtual void PrintFields() {
      Console.WriteLine("x = {0}", x);
   }
}
class B: A
{
   int y;
   public override void PrintFields() {
      base.PrintFields();
      Console.WriteLine("y = {0}", y);
   }
}

the base.PrintFields() invocation in B invokes the PrintFields method declared in A. A base-access disables the virtual invocation mechanism and simply treats the base method as a non-virtual method. Had the invocation in B been written ((A)this).PrintFields(), it would recursively invoke the PrintFields method declared in B, not the one declared in A.

Only by including an override modifier can a method override another method. In all other cases, a method with the same signature as an inherited method simply hides the inherited method. In the example

class A
{
   public virtual void F() {}
}
class B: A
{
   public virtual void F() {}      // Warning, hiding inherited F()
}

the F method in B does not include an override modifier and therefore does not override the F method in A. Rather, the F method in B hides the method in A, and a warning is reported because the declaration does not include a new modifier.

In the example

class A
{
   public virtual void F() {}
}
class B: A
{
   new private void F() {}         // Hides A.F within B
}
class C: B
{
   public override void F() {}   // Ok, overrides A.F
}

the F method in B hides the virtual F method inherited from A. Since the new F in B has private access, its scope only includes the class body of B and does not extend to C. The declaration of F in C is therefore permitted to override the F inherited from A.


10.5.5 Abstract methods

When an instance method declaration includes an abstract modifier, the method is said to be an abstract method. An abstract method is implicitly also a virtual method.

An abstract method declaration introduces a new virtual method but does not provide an implementation of the method. Instead, non-abstract derived classes are required to provide their own implementation by overriding the method. Because an abstract method provides no actual implementation, the method-body of an abstract method simply consists of a semicolon.

Abstract method declarations are only permitted in abstract classes (§10.1.1.1).

It is an error for an abstract method declaration to include either the static or virtual modifiers.

In the example

public abstract class Shape
{
   public abstract void Paint(Graphics g, Rectangle r);
}
public class Ellipse: Shape
{
   public override void Paint(Graphics g, Rectangle r) {
      g.drawEllipse(r);
   }
}
public class Box: Shape
{
   public override void Paint(Graphics g, Rectangle r) {
      g.drawRect(r);
   }
}

the Shape class defines the abstract notion of a geometrical shape object that can paint itself. The Paint method is abstract because there is no meaningful default implementation. The Ellipse and Box classes are concrete Shape implementations. Because theses classes are non-abstract, they are required to override the Paint method and provide an actual implementation.

It is an error for a base-access (§7.5.8) to reference an abstract method. In the example

class A
{
   public abstract void F();
}
class B: A
{
   public override void F() {
      base.F();                  // Error, base.F is abstract
   }
}

an error is reported for the base.F() invocation because it references an abstract method.

An abstract method declaration is permitted to override a virtual method. This allows an abstract class to force re-implementation of the method in derived classes, and makes the original implementation of the method unavailable. In the example

class A
{
   public virtual void F() {
      Console.WriteLine("A.F");
   }
}
abstract class B: A
{
   public abstract override void F();
}
class C: B
{
   public override void F() {
      Console.WriteLine("C.F");
   }
}

the class A declares a virtual method, the class B override this method with an abstract method, and the class C overrides to provide its own implementation.


10.5.6 External methods

When a method declaration includes an extern modifier, the method is said to be an external method. External methods are implemented externally, using a language other than C#. Because an external method declaration provides no actual implementation, the method-body of an external method simply consists of a semicolon.

The extern modifier is typically used in conjunction with a DllImport attribute (§19.6), allowing external methods to be implemented by DLLs (Dynamic Link Libraries). The execution environment may support other mechanisms whereby implementations of external methods can be provided.

It is an error for an external method declaration to also include the abstract modifier. When an external method includes a DllImport attribute, the method declaration must also include a static modifier.

This example demonstrates use of the extern modifier and the DllImport attribute:

class Path
{
   [DllImport("kernel32", setLastError=true)]
   static extern bool CreateDirectory(string name, SecurityAttributes sa);
   [DllImport("kernel32", setLastError=true)]
   static extern bool RemoveDirectory(string name);
   [DllImport("kernel32", setLastError=true)]
   static extern int GetCurrentDirectory(int bufSize, StringBuilder buf);
   [DllImport("kernel32", setLastError=true)]
   static extern bool SetCurrentDirectory(string name);
}


10.5.7 Method body

The method-body of a method declaration consists either of a block or a semicolon.

Abstract and external method declarations do not provide a method implementation, and the method body of an abstract or external method simply consists of a semicolon. For all other methods, the method body is a block (§8.2) that contains the statements to execute when the method is invoked.

When the return type of a method is void, return statements (§8.9.4) in the method body are not permitted to specify an expression. If execution of the method body of a void method completes normally (that is, if control flows off the end of the method body), the method simply returns to the caller.

When the return type of a method is not void, each return statement in the method body must specify an expression of a type that is implicitly convertible to the return type. Execution of the method body of a value-returning method is required to terminate in a return statement that specifies an expression, or in a throw statement that throws an exception. It is an error if execution of the method body can complete normally. In other words, in a value-returning method, control is not permitted to flow off the end of the method body.

In the example

class A
{
   public int F() {}         // Error, return value required
   public int G() {
      return 1;
   }
   public int H(bool b) {
      if (b) {
         return 1;
      }
      else {
         return 0;
      }
   }
}

the value-returning F method is in error because control can flow off the end of the method body. The G and H methods are correct because all possible execution paths end in a return statement that specifies a return value.


10.5.8 Method overloading

The method overload resolution rules are described in §7.4.2.


10.6 Properties

A property is a member that provides access to an attribute of an object or a class. Examples of properties include the length of a string, the size of a font, the caption of a window, the name of a customer, and so on. Properties are a natural extension of fields—both are named members with associated types, and the syntax for accessing fields and properties is the same. However, unlike fields, properties do not denote storage locations. Instead, properties have accessors that specify the statements to execute in order to read or write their values. Properties thus provide a mechanism for associating actions with the reading and writing of an object’s attributes, and they furthermore permit such attributes to be computed.

Properties are declared using property-declarations:

property-declaration:
attributesopt property-modifiersopt type member-name { accessor-declarations }
property-modifiers:
property-modifier
property-modifiers property-modifier
property-modifier:
new
public
protected
internal
private
static
virtual
override
abstract
member-name:
identifier
interface-type . identifier

A property-declaration may include a set of attributes (§17), a new modifier (§10.2.2), a valid combination of the four access modifiers (§10.2.3), and one of the static (§10.6.1), virtual (§10.6.2), override (§10.6.3), or abstract (§10.6.4) modifiers.

The type of a property declaration specifies the type of the property introduced by the declaration, and the member-name specifies the name of the property. Unless the property is an explicit interface member implementation, the member-name is simply an identifier. For an explicit interface member implementation (§13.4.1) , the member-name consists of an interface-type followed by a “.” and an identifier.

The type of a property must be at least as accessible as the property itself (§3.3.4).

The accessor-declarations, which must be enclosed in “{” and “}” tokens, declare the accessors (§10.6.5) of the property. The accessors specify the executable statements associated with reading and writing the property.

Even though the syntax for accessing a property is the same as that for a field, a property is not classified as a variable. Thus, it is not possible to pass a property as a ref or out parameter.


10.6.1 Static properties

When a property declaration includes a static modifier, the property is said to be a static property. When no static modifier is present, the property is said to be an instance property.

A static property is not associated with a specific instance, and it is an error to refer to this in the accessors of a static property. It is furthermore an error to include a virtual, abstract, or override modifier on a static property.

An instance property is associated with a given instance of a class, and this instance can be accessed as this (§7.5.7) in the accessors of the property.

When a property is referenced in a member-access (§7.5.4) of the form E.M, if M is a static property, E must denote a type, and if M is an instance property, E must denote an instance.

The differences between static and instance members are further discussed in §10.2.5.


10.6.2 Virtual properties

When an instance property declaration includes a virtual modifier, the property is said to be a virtual property. When no virtual modifier is present, the property is said to be a non-virtual property.

It is an error for a property declaration that includes the virtual modifier to also include any one of the static, abstract, or override modifiers.

The implementation of a non-virtual property is invariant: The implementation is the same whether the property is accessed on an instance of the class in which it is declared or an instance of a derived class. In contrast, the implementation of a virtual property can be changed by derived classes. The process of changing the implementation of an inherited virtual property is known as overriding the property (§10.6.3).

For every virtual property declared in or inherited by a class, there exists a most derived implementation of the property with respect to that class. The most derived implementation of a virtual property P with respect to a class R is determined as follows:

  • If R contains the introducing virtual declaration of P, then this is the most derived implementation of P.
  • Otherwise, if R contains an override of P, then this is the most derived implementation of P.
  • Otherwise, the most derived implementation of P is the same as that of the direct base class of R.

Because properties are allowed to hide inherited properties, it is possible for a class to contain several virtual properties with the same signature. This does not present an ambiguity problem, since all but the most derived property are hidden.


10.6.3 Override properties

When an instance property declaration includes an override modifier, the property is said to be an override property. An override property overrides an inherited virtual property with the same signature. Whereas a virtual property declaration introduces a new property, an override property declaration specializes an existing inherited virtual property by providing a new implementation of the property accessor or accessors.

It is an error for an override property declaration to include any one of the new, static, or virtual modifiers.

The property overridden by an override declaration is known as the overridden base property. For an override property P declared in a class C, the overridden base property is determined by examining each base class of C, starting with the direct base class of C and continuing with each successive direct base class, until an accessible property with the same signature as P is located. For purposes of locating the overridden base property, a property is considered accessible if it is public, if it is protected, if it is protected internal, or if it is internal and declared in the same program as C.

A compile-time error occurs unless all of the following are true for an override declaration:

  • An overridden base property can be located as described above.
  • The overridden base property is a virtual, abstract, or override property. In other words, the overridden base property cannot be static or non-virtual.
  • The override declaration and the overridden base property have the same declared accessibility. In other words, an override declaration cannot change the accessibility of the property.
  • The override declaration and the overridden base property have the same accessors. In other words, an override declaration cannot change whether the property is read-write, read-only, or write-only.

An override declaration can access the overridden base property using a base-access (§7.5.8). In the example

class A
{
   int pvalue = 1;
   public virtual int P {
      get { return pvalue; }
      set { pvalue = value; }
   }
}
class B: A
{
   public override int P {
      get { return base.P; }
      set { base.P = value; }
   }
}

the base.P accesses in B use the get and set accessors for the P property declared in A. A base-access disables the virtual invocation mechanism and simply treats the base property as a non-virtual property. Had the accesses in B been written ((A)this).P, it would recursively access the P property declared in B, not the one declared in A.

Only by including an override modifier can a property override another property. In this way, properties behave like methods, as specified in §10.5.4.


10.6.4 Abstract properties

When an instance property declaration includes an abstract modifier, the property is said to be an abstract property. An abstract property is implicitly also a virtual property.

An abstract property declaration introduces a new virtual property but does not provide an implementation of the property’s accessor or accessors. Instead, non-abstract derived classes are required to provide their own implementation for the accessor or accessors by overriding the property. Because an accessor for an abstract property declaration provides no actual implementation, its accessor-body simply consists of a semicolon.

Abstract property declarations are only permitted in abstract classes (§10.1.1.1).

It is an error for an abstract property declaration to include the static modifier or the virtual modifier.

It is an error for a base-access (§7.5.8) to reference an abstract property. In the example

class A
{
   public abstract int P { get; set; }
}
class B: A
{
   public override int P {
      get { return base.P; }      // Error, base.P is abstract
      set { base.P = value; }      // Error, base.P is abstract
   }
}

an error is reported for the base.P accesses because they reference an abstract property.


10.6.5 Accessors

The accessor-declarations of a property specify the executable statements associated with reading and writing the property.

accessor-declarations:
get-accessor-declaration set-accessor-declarationopt
set-accessor-declaration get-accessor-declarationopt
get-accessor-declaration:
attributesopt get accessor-body
set-accessor-declaration:
attributesopt set accessor-body
accessor-body:
block
;

The accessor declarations consist of a get-accessor-declaration, a set-accessor-declaration, or both. Each accessor declaration consists of the token get or set followed by an accessor-body. For abstract properties, the accessor-body for each accessor specified is simply a semicolon. For all other accessors, the accessor-body is a block which specifies the statements to execute when the accessor is invoked.

A get accessor corresponds to a parameterless method with a return value of the property type. Except as the target of an assignment, when a property is referenced in an expression, the get accessor of the property is invoked to compute the value of the property (§7.1.1). The body of a get accessor must conform to the rules for value-returning methods described in §10.5.7. In particular, all return statements in the body of a get accessor must specify an expression that is implicitly convertible to the property type. Furthermore, a get accessor is required to terminate in a return statement or a throw statement, and control is not permitted to flow off the end of the get accessor’s body.

A set accessor corresponds to a method with a single value parameter of the property type and a void return type. The implicit parameter of a set accessor is always named value. When a property is referenced as the target of an assignment, the set accessor is invoked with an argument that provides the new value (§7.13.1). The body of a set accessor must conform to the rules for void methods described in §10.5.7. In particular, return statements in the set accessor body are not permitted to specify an expression. Since a set accessor implicitly has a parameter named value, it is an error for a local variable declaration in a set accessor to use that name.

Based on the presence or absence of the get and set accessors, a property is classified as follows:

  • A property that includes both a get accessor and a set accessor is said to be a read-write property.
  • A property that has only a get accessor is said to be a read-only property. It is an error for a read-only property to be the target of an assignment.
  • A property that has only a set accessor is said to be a write-only property. Except as the target of an assignment, it is an error to reference a write-only property in an expression.

In the example

public class Button: Control
{
   private string caption;
   public string Caption {
      get {
         return caption;
      }
      set {
         if (caption != value) {
            caption = value;
            Repaint();
         }
      }
   }
   public override void Paint(Graphics g, Rectangle r) {
      // Painting code goes here
   }
}

the Button control declares a public Caption property. The get accessor of the Caption property returns the string stored in the private caption field. The set accessor checks if the new value is different from the current value, and if so, it stores the new value and repaints the control. Properties often follow the pattern shown above: The get accessor simply returns a value stored in a private field, and the set accessor modifies the private field and then performs any additional actions required to fully update the state of the object.

Given the Button class above, the following is an example of use of the Caption property:

Button okButton = new Button();
okButton.Caption = "OK";         // Invokes set accessor
string s = okButton.Caption;      // Invokes get accessor

Here, the set accessor is invoked by assigning a value to the property, and the get accessor is invoked by referencing the property in an expression.

The get and set accessors of a property are not distinct members, and it is not possible to declare the accessors of a property separately. The example

class A
{
   private string name;
   public string Name {            // Error, duplicate member name
      get { return name; }
   }
   public string Name {            // Error, duplicate member name
      set { name = value; }
   }
}

does not declare a single read-write property. Rather, it declares two properties with the same name, one read-only and one write-only. Since two members declared in the same class cannot have the same name, the example causes a compile-time error to occur.

When a derived class declares a property by the same name as an inherited property, the derived property hides the inherited property with respect to both reading and writing. In the example

class A
{
   public int P {
      set {...}
   }
}
class B: A
{
   new public int P {
      get {...}
   }
}

the P property in B hides the P property in A with respect to both reading and writing. Thus, in the statements

B b = new B();
b.P = 1;         // Error, B.P is read-only
((A)b).P = 1;   // Ok, reference to A.P

the assignment to b.P causes an error to be reported, since the read-only P property in B hides the write-only P property in A. Note, however, that a cast can be used to access the hidden P property.

Unlike public fields, properties provide a separation between an object’s internal state and its public interface. Consider the example:

class Label
{
   private int x, y;
   private string caption;
   public Label(int x, int y, string caption) {
      this.x = x;
      this.y = y;
      this.caption = caption;
   }
   public int X {
      get { return x; }
   }
   public int Y {
      get { return y; }
   }
   public Point Location {
      get { return new Point(x, y); }
   }
   public string Caption {
      get { return caption; }
   }
}

Here, the Label class uses two int fields, x and y, to store its location. The location is publicly exposed both as an X and a Y property and as a Location property of type Point. If, in a future version of Label, it becomes more convenient to store the location as a Point internally, the change can be made without affecting the public interface of the class:

class Label
{
   private Point location;
   private string caption;
   public Label(int x, int y, string caption) {
      this.location = new Point(x, y);
      this.caption = caption;
   }
   public int X {
      get { return location.x; }
   }
   public int Y {
      get { return location.y; }
   }
   public Point Location {
      get { return location; }
   }
   public string Caption {
      get { return caption; }
   }
}

Had x and y instead been public readonly fields, it would have been impossible to make such a change to the Label class.

Exposing state through properties is not necessarily any less efficient than exposing fields directly. In particular, when a property is non-virtual and contains only a small amount of code, the execution environment may replace calls to accessors with the actual code of the accessors. This process is known as inlining, and it makes property access as efficient as field access, yet preserves the increased flexibility of properties.

Since invoking a get accessor is conceptually equivalent to reading the value of a field, it is considered bad programming style for get accessors to have observable side-effects. In the example

class Counter
{
   private int next;
   public int Next {
      get { return next++; }
   }
}

the value of the Next property depends on the number of times the property has previously been accessed. Thus, accessing the property produces an observable side-effect, and the property should instead be implemented as a method.

The “no side-effects” convention for get accessors doesn’t mean that get accessors should always be written to simply return values stored in fields. Indeed, get accessors often compute the value of a property by accessing multiple fields or invoking methods. However, a properly designed get accessor performs no actions that cause observable changes in the state of the object.

Properties can be used to delay initialization of a resource until the moment it is first referenced. For example:

using System.IO;
public class Console
{
   private static TextReader reader;
   private static TextWriter writer;
   private static TextWriter error;
   public static TextReader In {
      get {
         if (reader == null) {
            reader = new StreamReader(File.OpenStandardInput());
         }
         return reader;
      }
   }
   public static TextWriter Out {
      get {
         if (writer == null) {
            writer = new StreamWriter(File.OpenStandardOutput());
         }
         return writer;
      }
   }
   public static TextWriter Error {
      get {
         if (error == null) {
            error = new StreamWriter(File.OpenStandardError());
         }
         return error;
      }
   }
}

The Console class contains three properties, In, Out, and Error, that represent the standard input, output, and error devices, respectively. By exposing these members as properties, the Console class can delay their initialization until they are actually used. For example, upon first referencing the Out property, as in

Console.Out.WriteLine("Hello world");

the underlying TextWriter for the output device is created. But if the application makes no reference to the In and Error properties, then no objects are created for those devices.


10.7 Events

An event is a member that enables an object or class to provide notifications. Clients can attach executable code for events by supplying event handlers. Events are declared using event-declarations:

event-declaration:
event-field-declaration
event-property-declaration
event-field-declaration:
attributesopt event-modifiersopt event type variable-declarators ;
event-property-declaration:
attributesopt event-modifiersopt event type member-name { accessor-declarations }
event-modifiers:
event-modifier
event-modifiers event-modifier
event-modifier:
new
public
protected
internal
private
static

An event declaration is either an event-field-declaration or an event-property-declaration. In both cases, the declaration may include a set of attributes (§17), a new modifier (§10.2.2), a valid combination of the four access modifiers (§10.2.3), and a static modifier (§10.2.5).

The type of an event declaration must be a delegate-type (§15), and that delegate-type must be at least as accessible as the event itself (§3.3.4).

An event field declaration corresponds to a field-declaration (§10.4) that declares one or more fields of a delegate type. The readonly modifier is not permitted in an event field declaration.

An event property declaration corresponds to a property-declaration (§10.6) that declares a property of a delegate type. The member-name and accessor-declarations are equivalent to those of a property declaration, except that an event property declaration must include both a get accessor and a set accessor, and that the virtual, override, and abstract modifiers are not permitted.

Within the program text of the class or struct that contains an event member declaration, the event member corresponds exactly to a private field or property of a delegate type, and the member can thus be used in any context that permits a field or property.

Outside the program text of the class or struct that contains an event member declaration, the event member can only be used as the left hand operand of the += and -= operators (§7.13.3). These operators are used to attach or remove event handlers to or from an event member, and the access modifiers of the event member control the contexts in which the operations are permitted.

Since += and -= are the only operations that are permitted on an event member outside the type that declares the event member, external code can append and remove handlers for an event, but cannot in any other way obtain or modify the value of the underlying event field or event property.

In the example

public delegate void EventHandler(object sender, Event e);
public class Button: Control
{
   public event EventHandler Click;
   protected void OnClick(Event e) {
      if (Click != null) Click(this, e);
   }
   public void Reset() {
      Click = null;
   }
}

there are no restrictions on usage of the Click event field within the Button class. As the example demonstrates, the field can be examined, modified, and used in delegate invocation expressions. The OnClick method in the Button class “raises” the Click event. The notion of raising an event is precisely equivalent to invoking the delegate represented by the event member—thus, there are no special language constructs for raising events. Note that the delegate invocation is preceded by a check that ensures the delegate is non-null.

Outside the declaration of the Button class, the Click member can only be used on the left hand side of the += and -= operators, as in

b.Click += new EventHandler(...);

which appends a delegate to the invocation list of the Click event, and

b.Click -= new EventHandler(...);

which removes a delegate from the invocation list of the Click event.

In an operation of the form x += y or x -= y, when x is an event member and the reference takes place outside the type that contains the declaration of x, the result of the operation is void (as opposed to the value of x after the assignment). This rule prohibits external code from indirectly examining the underlying delegate of an event member.

The following example shows how event handlers are attached to instances of the Button class above:

public class LoginDialog: Form
{
   Button OkButton;
   Button CancelButton;
   public LoginDialog() {
      OkButton = new Button(...);
      OkButton.Click += new EventHandler(OkButtonClick);
      CancelButton = new Button(...);
      CancelButton.Click += new EventHandler(CancelButtonClick);
   }
   void OkButtonClick(object sender, Event e) {
      // Handle OkButton.Click event
   }
   void CancelButtonClick(object sender, Event e) {
      // Handle CancelButton.Click event
   }
}

Here, the LoginDialog constructor creates two Button instances and attaches event handlers to the Click events.

Event members are typically fields, as in the Button example above. In cases where the storage cost of one field per event is not acceptable, a class can declare event properties instead of event fields and use a private mechanism for storing the underlying delegates. (In scenarios where most events are unhandled, using a field per event may not be acceptable. The ability to use properties rather than fields allows for space vs. speed tradeoffs to be made by the developer.)

In the example

class Control: Component
{
   // Unique keys for events
   static readonly object mouseDownEventKey = new object();
   static readonly object mouseUpEventKey = new object();
   // Return event handler associated with key
   protected Delegate GetEventHandler(object key) {...}
   // Set event handler associated with key
   protected void SetEventHandler(object key, Delegate handler) {...}
   // MouseDown event property
   public event MouseEventHandler MouseDown {
      get {
         return (MouseEventHandler)GetEventHandler(mouseDownEventKey);
      }
      set {
         SetEventHandler(mouseDownEventKey, value);
      }
   }
   // MouseUp event property
   public event MouseEventHandler MouseUp {
      get {
         return (MouseEventHandler)GetEventHandler(mouseUpEventKey);
      }
      set {
         SetEventHandler(mouseUpEventKey, value);
      }
   }
}

the Control class implements an internal storage mechanism for events. The SetEventHandler method associates a delegate value with a key, and the GetEventHandler method returns the delegate currently associated with a key. Presumably the underlying storage mechanism is designed such that there is no cost for associating a null delegate value with a key, and thus unhandled events consume no storage.


10.8 Indexers

An indexer is a member that enables an object to be indexed in the same way as an array. Indexers are declared using indexer-declarations:

indexer-declaration:
attributesopt indexer-modifiersopt indexer-declarator { accessor-declarations }
indexer-modifiers:
indexer-modifier
indexer-modifiers indexer-modifier
indexer-modifier:
new
public
protected
internal
private
virtual
override
abstract
indexer-declarator:
type this [ formal-parameter-list ]
type interface-type . this [ formal-parameter-list ]

An indexer-declaration may include a set of attributes (§17), a new modifier (§10.2.2), a valid combination of the four access modifiers (§10.2.3), and one of the virtual (§10.8.2), override (§10.8.3), or abstract (§10.8.4) modifiers.

The type of an indexer declaration specifies the element type of the indexer introduced by the declaration. Unless the indexer is an explicit interface member implementation, the type is followed by the keyword this. For an explicit interface member implementation, the type is followed by an interface-type, a “.”, and the keyword this. Unlike other members, indexers do not have user-defined names.

The formal-parameter-list specifies the parameters of the indexer. The formal parameter list of an indexer corresponds to that of a method (§10.5.1), except that at least one parameter must be specified, and that the ref and out parameter modifiers are not permitted.

The type of an indexer and each of the types referenced in the formal-parameter-list must be at least as accessible as the indexer itself (§3.3.4).

The accessor-declarations, which must be enclosed in “{” and “}” tokens, declare the accessors of the indexer. The accessors specify the executable statements associated with reading and writing indexer elements.

Even though the syntax for accessing an indexer element is the same as that for an array element, an indexer element is not classified as a variable. Thus, it is not possible to pass an indexer element as a ref or out parameter.

The formal parameter list of an indexer defines the signature (§3.4) of the indexer. Specifically, the signature of an indexer consists of the number and types of its formal parameters. The element type is not part of an indexer’s signature, nor are the names of the formal parameters.

The signature of an indexer must differ from the signatures of all other indexers declared in the same class.

Indexers and properties are very similar in concept, but differ in the following ways:

  • A property is identified by its name, whereas an indexer is identified by its signature.
  • A property is accessed through a simple-name (§7.5.2) or a member-access (§7.5.4), whereas an indexer element is accessed through an element-access (§7.5.6.2).
  • A property can be a static member, whereas an indexer is always an instance member.
  • A get accessor of a property corresponds to a method with no parameters, whereas a get accessor of an indexer corresponds to a method with the same formal parameter list as the indexer.
  • A set accessor of a property corresponds to a method with a single parameter named value, whereas a set accessor of an indexer corresponds to a method with the same formal parameter list as the indexer, plus an additional parameter named value.
  • It is an error for an indexer accessor to declare a local variable with the same name as an indexer parameter.

With these differences in mind, all rules defined in §10.6.5 apply to indexer accessors as well as property accessors.

The example below declares a BitArray class that implements an indexer for accessing the individual bits in the bit array.

class BitArray
{
   int[] bits;
   int length;
   public BitArray(int length) {
      if (length < 0) throw new ArgumentException();
      bits = new int[((length - 1) >> 5) + 1];
      this.length = length;
   }
   public int Length {
      get { return length; }
   }
   public bool this[int index] {
      get {
         if (index < 0 || index >= length) {
            throw new IndexOutOfRangeException();
         }
         return (bits[index >> 5] & 1 << index) != 0;
      }
      set {
         if (index < 0 || index >= length) {
            throw new IndexOutOfRangeException();
         }
         if (value) {
            bits[index >> 5] |= 1 << index;
         }
         else {
            bits[index >> 5] &= ~(1 << index);
         }
      }
   }
}

An instance of the BitArray class consumes substantially less memory than a corresponding bool[] (each value occupies only one bit instead of one byte), but it permits the same operations as a bool[].

The following CountPrimes class uses a BitArray and the classical “sieve” algorithm to compute the number of primes between 1 and a given maximum:

class CountPrimes
{
   static int Count(int max) {
      BitArray flags = new BitArray(max + 1);
      int count = 1;
      for (int i = 2; i <= max; i++) {
         if (!flags[i]) {
            for (int j = i * 2; j <= max; j += i) flags[j] = true;
            count++;
         }
      }
      return count;
   }
   static void Main(string[] args) {
      int max = int.Parse(args[0]);
      int count = Count(max);
      Console.WriteLine("Found {0} primes between 1 and {1}", count, max);
   }
}

Note that the syntax for accessing elements of the BitArray is precisely the same as for a bool[].


10.8.1 Indexer overloading

The indexer overload resolution rules are described in §7.4.2.


10.8.2 Virtual indexers

When an indexer declaration includes a virtual modifier, the indexer is said to be a virtual indexer. When no virtual modifier is present, the indexer is said to be a non-virtual indexer.

It is an error for a indexer declaration that includes the virtual modifier to also include any one of the abstract or override modifiers.

The implementation of a non-virtual indexer is invariant: The implementation is the same whether the indexer is accessed on an instance of the class in which it is declared or an instance of a derived class. In contrast, the implementation of a virtual indexer can be changed by derived classes. The process of changing the implementation of an inherited virtual indexer is known as overriding the indexer.

Because indexers are allowed to hide inherited indexers, it is possible for a class to contain several virtual indexers with the same signature.


10.8.3 Override indexers

When an indexer declaration includes an override modifier, the indexer is said to be an override indexer. An override indexer overrides an inherited virtual indexer with the same signature. Whereas a virtual indexer declaration introduces a new indexer, an override indexer declaration specializes an existing inherited virtual indexer by providing a new implementation of the indexer accessor or accessors.

It is an error for an override indexer declaration to include either the new or virtual modifiers.

The indexer overridden by an override declaration is known as the overridden base indexer. For an override indexer declared in a class C, the overridden base indexer is determined by examining each base class of C, starting with the direct base class of C and continuing with each successive direct base class, until an accessible indexer with the same signature is located. For purposes of locating the overridden base indexer, a indexer is considered accessible if it is public, if it is protected, if it is protected internal, or if it is internal and declared in the same program as C.

A compile-time error occurs unless all of the following are true for an override declaration:

  • An overridden base indexer can be located as described above.
  • The overridden base indexer is a virtual, abstract, or override indexer. In other words, the overridden base indexer cannot be non-virtual.
  • The override declaration and the overridden base indexer have the same declared accessibility. In other words, an override declaration cannot change the accessibility of the indexer.
  • The override declaration and the overridden base indexer have the same accessors. In other words, an override declaration cannot change whether the indexer is read-write, read-only, or write-only.

An override declaration can access the overridden base indexer using a base-access (§7.5.8). In the example

class A
{
   public virtual int this[int index] {
      get { return index; }
   }
}
class B: A
{
   public override int this[int index] {
      get { return base[index] + 1; }
   }
}

the base[index] access in B uses the get accessors for the indexer declared in A. A base-access disables the virtual invocation mechanism and simply treats the base indexer as a non-virtual indexer. Had the accesses in B been written ((A)this)[index], it would recursively access the indexer declared in B, not the one declared in A.

Only by including an override modifier can a indexer override another indexer. In this way, indexers behave like methods, as specified in §10.5.4.


10.8.4 Abstract indexers

When an indexer declaration includes an abstract modifier, the indexer is said to be an abstract indexer. An abstract indexer is implicitly also a virtual indexer.

An abstract indexer declaration introduces a new virtual indexer but does not provide an implementation of the indexer’s accessor or accessors. Instead, non-abstract derived classes are required to provide their own implementation for the accessor or accessors by overriding the indexer. Because an accessor for an abstract indexer declaration provides no actual implementation, its accessor-body simply consists of a semicolon.

Abstract indexer declarations are only permitted in abstract classes (§10.1.1.1).

It is an error for an abstract indexer declaration to include the virtual modifier.

It is an error for a base-access (§7.5.8) to reference an abstract indexer. In the example

class A
{
   public abstract int this[int index] { get; }
}
class B: A
{
   public override int this[int index] {
      get { return base[index] + 1; }   // Error, base[index] is abstract
   }
}

an error is reported for the base.P accesses because they reference an abstract indexer.


10.9 Operators

An operator is a member that defines the meaning of an expression operator that can be applied to instances of the class. Operators are declared using operator-declarations:

operator-declaration:
attributesopt operator-modifiers operator-declarator block
operator-modifiers:
public static
static public
operator-declarator:
unary-operator-declarator
binary-operator-declarator
conversion-operator-declarator
unary-operator-declarator:
type operator overloadable-unary-operator ( type identifier )
overloadable-unary-operator: one of
+ - ! ~ ++ -- true false
binary-operator-declarator:
type operator overloadable-binary-operator ( type identifier , type identifier )
overloadable-binary-operator: one of
+ - * / % & | ^ << >> == != > < >= <=
conversion-operator-declarator:
implicit operator type ( type identifier )
explicit operator type ( type identifier )

There are three categories of operators: Unary operators (§10.9.1), binary operators (§10.9.2), and conversion operators (§10.9.3).

The following rules apply to all operator declarations:

  • An operator declaration must include both a public and a static modifier, and is not permitted to include any other modifiers.
  • The parameter(s) of an operator must be value parameters. It is an error to for an operator declaration to specify ref or out parameters.
  • The signature of an operator must differ from the signatures of all other operators declared in the same class.
  • All types referenced in an operator declaration must be at least as accessible as the operator itself (§3.3.4).

Each operator category imposes additional restrictions, as described in the following sections.

Like other members, operators declared in a base class are inherited by derived classes. Because operator declarations always require the class or struct in which the operator is declared to participate in the signature of the operator, it is not possible for an operator declared in a derived class to hide an operator declared in a base class. Thus, the new modifier is never required, and therefore never permitted, in an operator declaration.

For all operators, the operator declaration includes a block which specifies the statements to execute when the operator is invoked. The block of an operator must conform to the rules for value-returning methods described in §10.5.7.

Additional information on unary and binary operators can be found in §7.2.

Additional information on conversion operators can be found in §6.4.


10.9.1 Unary operators

The following rules apply to unary operator declarations, where T denotes the class or struct type that contains the operator declaration:

  • A unary +, -, !, or ~ operator must take a single parameter of type T and can return any type.
  • A unary ++ or -- operator must take a single parameter of type T and must return type T.
  • A unary true or false operator must take a single parameter of type T and must return type bool.

The signature of a unary operator consists of the operator token (+, -, !, ~, ++, --, true, or false) and the type of the single formal parameter. The return type is not part of a unary operator’s signature, nor is the name of the formal parameter.

The true and false unary operators require pair-wise declaration. An error occurs if a class declares one of these operators without also declaring the other. The true and false operators are further described in §7.16.


10.9.2 Binary operators

A binary operator must take two parameters, at least one of which must be of the class or struct type in which the operator is declared. A binary operator can return any type.

The signature of a binary operator consists of the operator token (+, -, *, /, %, &, |, ^, <<, >>, ==, !=, >, <, >=, or <=) and the types of the two formal parameters. The return type is not part of a binary operator’s signature, nor are the names of the formal parameters.

Certain binary operators require pair-wise declaration. For every declaration of either operator of a pair, there must be a matching declaration of the other operator of the pair. Two operator declarations match when they have the same return type and the same type for each parameter. The following operators require pair-wise declaration:

  • operator == and operator !=
  • operator > and operator <
  • operator >= and operator <=


10.9.3 Conversion operators

A conversion operator declaration introduces a user-defined conversion (§6.4) which augments the pre-defined implicit and explicit conversions.

A conversion operator declaration that includes the implicit keyword introduces a user-defined implicit conversion. Implicit conversions can occur in a variety of situations, including function member invocations, cast expressions, and assignments. This is described further in §6.1.

A conversion operator declaration that includes the explicit keyword introduces a user-defined explicit conversion. Explicit conversions can occur in cast expressions, and are described further in §6.2.

A conversion operator converts from a source type, indicated by the parameter type of the conversion operator, to a target type, indicated by the return type of the conversion operator. A class or struct is permitted to declare a conversion from a source type S to a target type T provided all of the following are true:

  • S and T are different types.
  • Either S or T is the class or struct type in which the operator declaration takes place.
  • Neither S nor T is object or an interface-type.
  • T is not a base class of S, and S is not a base class of T.

From the second rule it follows that a conversion operator must either convert to or from the class or struct type in which the operator is declared. For example, it is possible for a class or struct type C to define a conversion from C to int and from int to C, but not from int to bool.

It is not possible to redefine a pre-defined conversion. Thus, conversion operators are not allowed to convert from or to object because implicit and explicit conversions already exist between object and all other types. Likewise, neither of the source and target types of a conversion can be a base type of the other, since a conversion would then already exist.

User-defined conversions are not allowed to convert from or to interface-types. This restriction in particular ensures that no user-defined transformations occur when converting to an interface-type, and that a conversion to an interface-type succeeds only if the object being converted actually implements the specified interface-type.

The signature of a conversion operator consists of the source type and the target type. (Note that this is the only form of member for which the return type participates in the signature.) The implicit or explicit classification of a conversion operator is not part of the operator’s signature. Thus, a class or struct cannot declare both an implicit and an explicit conversion operator with the same source and target types.

In general, user-defined implicit conversions should be designed to never throw exceptions and never lose information. If a user-defined conversion can give rise to exceptions (for example because the source argument is out of range) or loss of information (such as discarding high-order bits), then that conversion should be defined as an explicit conversion.

In the example

public struct Digit
{
   byte value;
   public Digit(byte value) {
      if (value < 0 || value > 9) throw new ArgumentException();
      this.value = value;
   }
   public static implicit operator byte(Digit d) {
      return d.value;
   }
   public static explicit operator Digit(byte b) {
      return new Digit(b);
   }
}

the conversion from Digit to byte is implicit because it never throws exceptions or loses information, but the conversion from byte to Digit is explicit since Digit can only represent a subset of the possible values of a byte.


10.10 Instance constructors

An instance constructor is a member that implements the actions required to initialize an instance of a class. Constructors are declared using constructor-declarations:

constructor-declaration:
attributesopt constructor-modifiersopt constructor-declarator block
constructor-modifiers:
constructor-modifier
constructor-modifiers constructor-modifier
constructor-modifier:
public
protected
internal
private
constructor-declarator:
identifier ( formal-parameter-listopt ) constructor-initializeropt
constructor-initializer:
: base ( argument-listopt )
: this ( argument-listopt )

A constructor-declaration may include a set of attributes (§17) and a valid combination of the four access modifiers (§10.2.3).

The identifier of a constructor-declarator must name the class in which the constructor is declared. If any other name is specified, an error occurs.

The optional formal-parameter-list of a constructor is subject to the same rules as the formal-parameter-list of a method (§10.5). The formal parameter list defines the signature (§3.4) of a constructor and governs the process whereby overload resolution (§7.4.2) selects a particular constructor in an invocation.

Each of the types referenced in the formal-parameter-list of a constructor must be at least as accessible as the constructor itself (§3.3.4).

The optional constructor-initializer specifies another constructor to invoke before executing the statements given in the block of this constructor. This is described further in §10.10.1.

The block of a constructor declaration specifies the statements to execute in order to initialize a new instance of the class. This corresponds exactly to the block of an instance method with a void return type (§10.5.7).

Constructors are not inherited. Thus, a class has no other constructors than those that are actually declared in the class. If a class contains no constructor declarations, a default constructor is automatically provided (§10.10.4).

Constructors are invoked by object-creation-expressions (§7.5.10.1) and through constructor-initializers.


10.10.1 Constructor initializers

All constructors (except for the constructors of class object) implicitly include an invocation of another constructor immediately before the first statement in the block of the constructor. The constructor to implicitly invoke is determined by the constructor-initializer:

  • A constructor initializer of the form base(...) causes a constructor from the direct base class to be invoked. The constructor is selected using the overload resolution rules of §7.4.2. The set of candidate constructors consists of all accessible constructors declared in the direct base class. If the set of candidate constructors is empty, or if a single best constructor cannot be identified, an error occurs.
  • A constructor initializer of the form this(...) causes a constructor from the class itself to be invoked. The constructor is selected using the overload resolution rules of §7.4.2. The set of candidate constructors consists of all accessible constructors declared in the class itself. If the set of candidate constructors is empty, or if a single best constructor cannot be identified, an error occurs. If a constructor declaration includes a constructor initializer that invokes the constructor itself, an error occurs.

If a constructor has no constructor initializer, a constructor initializer of the form base() is implicitly provided. Thus, a constructor declaration of the form

C(...) {...}

is exactly equivalent to

C(...): base() {...}

The scope of the parameters given by the formal-parameter-list of a constructor declaration includes the constructor initializer of that declaration. Thus, a constructor initializer is permitted to access the parameters of the constructor. For example:

class A
{
   public A(int x, int y) {}
}
class B: A
{
   public B(int x, int y): base(x + y, x - y) {}
}

A constructor initializer cannot access the instance being created. It is therefore an error to reference this in an argument expression of the constructor initializer, as it is an error for an argument expression to reference any instance member through a simple-name.


10.10.2 Instance variable initializers

When a constructor has no constructor initializer or a constructor initializer of the form base(...), the constructor implicitly performs the initializations specified by the variable-initializers of the instance fields declared in the class. This corresponds to a sequence of assignments that are executed immediately upon entry to the constructor and before the implicit invocation of the direct base class constructor. The variable initializers are executed in the textual order they appear in the class declaration.


10.10.3 Constructor execution

It is useful to think of instance variable initializers and constructor initializers as statements that are automatically inserted before the first statement in the block of a constructor. The example

using System.Collections;
class A
{
   int x = 1, y = -1, count;
   public A() {
      count = 0;
   }
   public A(int n) {
      count = n;
   }
}
class B: A
{
   double sqrt2 = Math.Sqrt(2.0);
   ArrayList items = new ArrayList(100);
   int max;
   public B(): this(100) {
      items.Add("default");
   }
   public B(int n): base(n – 1) {
      max = n;
   }
}

contains several variable initializers and also contains constructor initializers of both forms (base and this). The example corresponds to the code shown below, where each comment indicates an automatically inserted statement (the syntax used for the automatically inserted constructor invocations isn’t valid, but merely serves to illustrate the mechanism).

using System.Collections;
class A
{
   int x, y, count;
   public A() {
      x = 1;                        // Variable initializer
      y = -1;                        // Variable initializer
      object();                     // Invoke object() constructor
      count = 0;
   }
   public A(int n) {
      x = 1;                        // Variable initializer
      y = -1;                        // Variable initializer
      object();                     // Invoke object() constructor
      count = n;
   }
}
class B: A
{
   double sqrt2;
   ArrayList items;
   int max;
   public B(): this(100) {
      B(100);                        // Invoke B(int) constructor
      items.Add("default");
   }
   public B(int n): base(n – 1) {
      sqrt2 = Math.Sqrt(2.0);         // Variable initializer
      items = new ArrayList(100);   // Variable initializer
      A(n – 1);                     // Invoke A(int) constructor
      max = n;
   }
}

Note that variable initializers are transformed into assignment statements, and that these assignment statements are executed before the invocation of the base class constructor. This ordering ensures that all instance fields are initialized by their variable initializers before any statements that have access to the instance are executed. For example:

class A
{
   public A() {
      PrintFields();
   }
   public virtual void PrintFields() {}
}
class B: A
{
   int x = 1;
   int y;
   public B() {
      y = -1;
   }
   public override void PrintFields() {
      Console.WriteLine("x = {0}, y = {1}", x, y);
   }
}

When new B() is used to create an instance of B, the following output is produced:

x = 1, y = 0

The value of x is 1 because the variable initializer is executed before the base class constructor is invoked. However, the value of y is 0 (the default value of an int) because the assignment to y is not executed until after the base class constructor returns.


10.10.4 Default constructors

If a class contains no constructor declarations, a default constructor is automatically provided. The default constructor simply invokes the parameterless constructor of the direct base class. If the direct base class does not have an accessible parameterless constructor, an error occurs. If the class is abstract then the declared accessibility for the default constructor is protected. Otherwise, the declared accessibility for the default constructor is public. Thus, the default constructor is always of the form

protected C(): base() {}

or

public C(): base() {}

where C is the name of the class.

In the example

class Message
{
   object sender;
   string text;
}

a default constructor is provided because the class contains no constructor declarations. Thus, the example is precisely equivalent to

class Message
{
   object sender;
   string text;
   public Message(): base() {}
}


10.10.5 Private constructors

When a class declares only private constructors it is not possible for other classes to derive from the class or create instances of the class (an exception being classes nested within the class). Private constructors are commonly used in classes that contain only static members. For example:

public class Trig
{
   private Trig() {}      // Prevent instantiation
   public const double PI = 3.14159265358979323846;
   public static double Sin(double x) {...}
   public static double Cos(double x) {...}
   public static double Tan(double x) {...}
}

The Trig class provides a grouping of related methods and constants, but is not intended to be instantiated. It therefore declares a single private constructor. At least one private constructor must be declared to suppress the automatic generation of a default constructor.


10.10.6 Optional constructor parameters

The this(...) form of constructor initializer is commonly used in conjunction with overloading to implement optional constructor parameters. In the example

class Text
{
   public Text(): this(0, 0, null) {}
   public Text(int x, int y): this(x, y, null) {}
   public Text(int x, int y, string s) {
      // Actual constructor implementation
   }
}

the first two constructors merely provide the default values for the missing arguments. Both use a this(...) constructor initializer to invoke the third constructor, which actually does the work of initializing the new instance. The effect is that of optional constructor parameters:

Text t1 = new Text();               // Same as Text(0, 0, null)
Text t2 = new Text(5, 10);            // Same as Text(5, 10, null)
Text t3 = new Text(5, 20, "Hello");


10.11 Destructors

A destructor is a member that implements the actions required to destruct an instance of a class. Destructors are declared using destructor-declarations:

destructor-declaration:
attributesopt ~ identifier ( ) block

A destructor-declaration may include a set of attributes (§17).

The identifier of a destructor-declarator must name the class in which the destructor is declared. If any other name is specified, an error occurs.

The block of a destructor declaration specifies the statements to execute in order to destruct an instance of the class. This corresponds exactly to the block of an instance method with a void return type (§10.5.7).

Destructors are not inherited. Thus, a class has no other destructors than those that are actually declared in the class.

Destructors are invoked automatically, and cannot be invoked explicitly. An instance becomes eligible for destruction when it is no longer possible for any code to use the instance. Execution of the destructor for the instance may occur at any time after the instance becomes eligible for destruction. When an instance is destructed, the destructors in an inheritance chain are called in order, from most derived to least derived.


10.12 Static constructors

A static constructor is a member that implements the actions required to initialize a class. Static constructors are declared using static-constructor-declarations:

static-constructor-declaration:
attributesopt static identifier ( ) block

A static-constructor-declaration may include a set of attributes (§17).

The identifier of a static-constructor-declaration must name the class in which the static constructor is declared. If any other name is specified, an error occurs.

The block of a static constructor declaration specifies the statements to execute in order to initialize the class. This corresponds exactly to the block of a static method with a void return type (§10.5.7).

Static constructors are not inherited.

Static constructors are invoked automatically, and cannot be invoked explicitly. The exact timing and ordering of static constructor execution is not defined, though several guarantees are provided:

  • The static constructor for a class is executed before any instance of the class is created.
  • The static constructor for a class is executed before any static member of the class is referenced.
  • The static constructor for a class is executed before the static constructor of any of its derived classes are executed.
  • The static constructor for a class never executes more than once.

The example

class Test
{
   static void Main() {
      A.F();
      B.F();
   }
}
class A
{
   static A() {
      Console.WriteLine("Init A");
   }
   public static void F() {
      Console.WriteLine("A.F");
   }
}
class B
{
   static B() {
      Console.WriteLine("Init B");
   }
   public static void F() {
      Console.WriteLine("B.F");
   }
}

could produce either the output:

Init A
A.F
Init B
B.F

or the output:

Init B
Init A
A.F
B.F

because the exact ordering of static constructor execution is not defined.

The example

class Test
{
   static void Main() {
      Console.WriteLine("1");
      B.G();
      Console.WriteLine("2");
   }
}
class A
{
   static A() {
      Console.WriteLine("Init A");
   }
}
class B: A
{
   static B() {
      Console.WriteLine("Init B");
   }
   public static void G() {
      Console.WriteLine("B.G");
   }
}

is guaranteed to produce the output:

Init A
Init B
B.G

because the static constructor for the class A must execute before the static constructor of the class B, which derives from it.


10.12.1 Class loading and initialization

It is possible to construct circular dependencies that allow static fields with variable initializers to be observed in their default value state.

The example

class A
{
   public static int X = B.Y + 1;
}
class B
{
   public static int Y = A.X + 1;
   static void Main() {
      Console.WriteLine("X = {0}, Y = {1}", A.X, B.Y);
   }
}

produces the output

X = 1, Y = 2

To execute the Main method, the system first loads class B. The static constructor of B proceeds to compute the initial value of Y, which recursively causes A to be loaded because the value of A.X is referenced. The static constructor of A in turn proceeds to compute the initial value of X, and in doing so fetches the default value of Y, which is zero. A.X is thus initialized to 1. The process of loading A then completes, returning to the calculation of the initial value of Y, the result of which becomes 2.

Had the Main method instead been located in class A, the example would have produced the output

X = 2, Y = 1

Circular references in static field initializers should be avoided since it is generally not possible to determine the order in which classes containing such references are loaded.


11. Structs

© 1987-2000 Microsoft Corporation. All rights reserved.


11.1 Struct declarations

struct-declaration:
attributesopt struct-modifiersopt struct identifier struct-interfacesopt struct-body ;opt


11.1.1 Struct modifiers

struct-modifiers:
struct-modifier
struct-modifiers struct-modifier
struct-modifier:
new
public
protected
internal
private


11.1.2 Interfaces

struct-interfaces:
: interface-type-list


11.1.3 Struct body

struct-body:
{ struct-member-declarationsopt }


11.2 Struct members

struct-member-declarations:
struct-member-declaration
struct-member-declarations struct-member-declaration
struct-member-declaration:
class-member-declaration


11.3 Struct examples

© 1987-2000 Microsoft Corporation. All rights reserved.


11.3.1 Database integer type

The DBInt struct below implements an integer type that can represent the complete set of values of the int type, plus an additional state that indicates an unknown value. A type with these characteristics is commonly used in databases.

public struct DBInt
{
   // The Null member represents an unknown DBInt value.
   public static readonly DBInt Null = new DBInt();
   // When the defined field is true, this DBInt represents a known value
   // which is stored in the value field. When the defined field is false,
   // this DBInt represents an unknown value, and the value field is 0.
   int value;
   bool defined;
   // Private constructor. Creates a DBInt with a known value.
   DBInt(int value) {
      this.value = value;
      this.defined = true;
   }
   // The IsNull property is true if this DBInt represents an unknown value.
   public bool IsNull { get { return !defined; } }
   // The Value property is the known value of this DBInt, or 0 if this
   // DBInt represents an unknown value.
   public int Value { get { return value; } }
   // Implicit conversion from int to DBInt.
   public static implicit operator DBInt(int x) {
      return new DBInt(x);
   }
   // Explicit conversion from DBInt to int. Throws an exception if the
   // given DBInt represents an unknown value.
   public static explicit operator int(DBInt x) {
      if (!x.defined) throw new InvalidOperationException();
      return x.value;
   }
   public static DBInt operator +(DBInt x) {
      return x;
   }
   public static DBInt operator -(DBInt x) {
      return x.defined? new DBInt(-x.value): Null;
   }
   public static DBInt operator +(DBInt x, DBInt y) {
      return x.defined && y.defined? new DBInt(x.value + y.value): Null;
   }
   public static DBInt operator -(DBInt x, DBInt y) {
      return x.defined && y.defined? new DBInt(x.value - y.value): Null;
   }
   public static DBInt operator *(DBInt x, DBInt y) {
      return x.defined && y.defined? new DBInt(x.value * y.value): Null;
   }
   public static DBInt operator /(DBInt x, DBInt y) {
      return x.defined && y.defined? new DBInt(x.value / y.value): Null;
   }
   public static DBInt operator %(DBInt x, DBInt y) {
      return x.defined && y.defined? new DBInt(x.value % y.value): Null;
   }
   public static DBBool operator ==(DBInt x, DBInt y) {
      return x.defined && y.defined?
         new DBBool(x.value == y.value): DBBool.Null;
   }
   public static DBBool operator !=(DBInt x, DBInt y) {
      return x.defined && y.defined?
         new DBBool(x.value != y.value): DBBool.Null;
   }
   public static DBBool operator >(DBInt x, DBInt y) {
      return x.defined && y.defined?
         new DBBool(x.value > y.value): DBBool.Null;
   }
   public static DBBool operator <(DBInt x, DBInt y) {
      return x.defined && y.defined?
         new DBBool(x.value < y.value): DBBool.Null;
   }
   public static DBBool operator >=(DBInt x, DBInt y) {
      return x.defined && y.defined?
         new DBBool(x.value >= y.value): DBBool.Null;
   }
   public static DBBool operator <=(DBInt x, DBInt y) {
      return x.defined && y.defined?
         new DBBool(x.value <= y.value): DBBool.Null;
   }
}


11.3.2 Database boolean type

The DBBool struct below implements a three-valued logical type. The possible values of this type are DBBool.True, DBBool.False, and DBBool.Null, where the Null member indicates an unknown value. Such three-valued logical types are commonly used in databases.

public struct DBBool
{
   // The three possible DBBool values.
   public static readonly DBBool Null = new DBBool(0);
   public static readonly DBBool False = new DBBool(-1);
   public static readonly DBBool True = new DBBool(1);
   // Private field that stores –1, 0, 1 for False, Null, True.
   int value;
   // Private constructor. The value parameter must be –1, 0, or 1.
   DBBool(int value) {
      this.value = value;
   }
   // Properties to examine the value of a DBBool. Return true if this
   // DBBool has the given value, false otherwise.
   public bool IsNull { get { return value == 0; } }
   public bool IsFalse { get { return value < 0; } }
   public bool IsTrue { get { return value > 0; } }
   // Implicit conversion from bool to DBBool. Maps true to DBBool.True and
   // false to DBBool.False.
   public static implicit operator DBBool(bool x) {
      return x? True: False;
   }
   // Explicit conversion from DBBool to bool. Throws an exception if the
   // given DBBool is Null, otherwise returns true or false.
   public static explicit operator bool(DBBool x) {
      if (x.value == 0) throw new InvalidOperationException();
      return x.value > 0;
   }
   // Equality operator. Returns Null if either operand is Null, otherwise
   // returns True or False.
   public static DBBool operator ==(DBBool x, DBBool y) {
      if (x.value == 0 || y.value == 0) return Null;
      return x.value == y.value? True: False;
   }
   // Inequality operator. Returns Null if either operand is Null, otherwise
   // returns True or False.
   public static DBBool operator !=(DBBool x, DBBool y) {
      if (x.value == 0 || y.value == 0) return Null;
      return x.value != y.value? True: False;
   }
   // Logical negation operator. Returns True if the operand is False, Null
   // if the operand is Null, or False if the operand is True.
   public static DBBool operator !(DBBool x) {
      return new DBBool(-x.value);
   }
   // Logical AND operator. Returns False if either operand is False,
   // otherwise Null if either operand is Null, otherwise True.
   public static DBBool operator &(DBBool x, DBBool y) {
      return new DBBool(x.value < y.value? x.value: y.value);
   }
   // Logical OR operator. Returns True if either operand is True, otherwise
   // Null if either operand is Null, otherwise False.
   public static DBBool operator |(DBBool x, DBBool y) {
      return new DBBool(x.value > y.value? x.value: y.value);
   }
   // Definitely true operator. Returns true if the operand is True, false
   // otherwise.
   public static bool operator true(DBBool x) {
      return x.value > 0;
   }
   // Definitely false operator. Returns true if the operand is False, false
   // otherwise.
   public static bool operator false(DBBool x) {
      return x.value < 0;
   }
}


12. Arrays

An array is a data structure that contains a number of variables which are accessed through computed indices. The variables contained in an array, also called the elements of the array, are all of the same type, and this type is called the element type of the array.

An array has a rank which determines the number of indices associated with each array element. The rank of an array is also referred to as the dimensions of the array. An array with a rank of one is called a single-dimensional array, and an array with a rank greater than one is called a multi-dimensional array.

Each dimension of an array has an associated length which is an integral number greater than or equal to zero. The dimension lengths are not part of the type of the array, but rather are established when an instance of the array type is created at run-time. The length of a dimension determines the valid range of indices for that dimension: For a dimension of length N, indices can range from 0 to N – 1 inclusive. The total number of elements in an array is the product of the lengths of each dimension in the array. If one or more of the dimensions of an array have a length of zero, the array is said to be empty.

The element type of an array can be any type, including an array type.


12.1 Array types

An array type is written as a non-array-type followed by one or more rank-specifiers:

array-type:
non-array-type rank-specifiers
non-array-type:
type
rank-specifiers:
rank-specifier
rank-specifiers rank-specifier
rank-specifier:
[ dim-separatorsopt ]
dim-separators:
,
dim-separators ,

A non-array-type is any type that is not itself an array-type.

The rank of an array type is given by the leftmost rank-specifier in the array-type: A rank-specifier indicates that the array is an array with a rank of one plus the number of “,” tokens in the rank-specifier.

The element type of an array type is the type that results from deleting the leftmost rank-specifier:

  • An array type of the form T[R] is an array with rank R and a non-array element type T.
  • An array type of the form T[R][R1]...[RN] is an array with rank R and an element type T[R1]...[RN].

In effect, the rank-specifiers are read from left to right before the final non-array element type. For example, the type int[][,,][,] is a single-dimensional array of three-dimensional arrays of two-dimensional arrays of int.

Arrays with a rank of one are called single-dimensional arrays. Arrays with a rank greater than one are called multi-dimensional arrays, and are also referred to as two-dimensional arrays, three-dimensional arrays, and so on.

At run-time, a value of an array type can be null or a reference to an instance of that array type.


12.1.1 The System.Array type

The System.Array type is the abstract base type of all array types. An implicit reference conversion (§6.1.4) exists from any array type to System.Array, and an explicit reference conversion (§6.2.3) exists from System.Array to any array type. Note that System.Array is itself not an array-type. Rather, it is a class-type from which all array-types are derived.

At run-time, a value of type System.Array can be null or a reference to an instance of any array type.


12.2 Array creation

Array instances are created by array-creation-expressions (§7.5.10.2) or by field or local variable declarations that include an array-initializer (§12.6).

When an array instance is created, the rank and length of each dimension are established and then remain constant for the entire lifetime of the instance. In other words, it is not possible to change the rank of an existing array instance, nor is it possible to resize its dimensions.

An array instance created by an array-creation-expression is always of an array type. The System.Array type is an abstract type that cannot be instantiated.

Elements of arrays created by array-creation-expressions are always initialized to their default value (§5.2).


12.3 Array element access

Array elements are accessed using element-access expressions (§7.5.6.1) of the form A[I1, I2, ..., IN], where A is an expression of an array type and each IX is an expression of type int, uint, long, ulong, or of a type that can be implicitly converted to one or more of these types.. The result of an array element access is a variable, namely the array element selected by the indices.

The elements of an array can be enumerated using a foreach statement (§8.8.4).


12.4 Array members

Every array type inherits the members declared by the System.Array type.


12.5 Array covariance

For any two reference-types A and B, if an implicit reference conversion (§6.1.4) or explicit reference conversion (§6.2.3) exists from A to B, then the same reference conversion also exists from the array type A[R] to the array type B[R], where R is any given rank-specifier (but the same for both array types). This relationship is known as array covariance. Array covariance in particular means that a value of an array type A[R] may actually be a reference to an instance of an array type B[R], provided an implicit reference conversion exists from B to A.

Because of array covariance, assignments to elements of reference type arrays include a run-time check which ensures that the value being assigned to the array element is actually of a permitted type (§7.13.1). For example:

class Test
{
   static void Fill(object[] array, int index, int count, object value) {
      for (int i = index; i < index + count; i++) array[i] = value;
   }
   static void Main() {
      string[] strings = new string[100];
      Fill(strings, 0, 100, "Undefined");
      Fill(strings, 0, 10, null);
      Fill(strings, 90, 10, 0);
   }
}

The assignment to array[i] in the Fill method implicitly includes a run-time check which ensures that the object referenced by value is either null or an instance of a type that is compatible with the actual element type of array. In Main, the first two invocations of Fill succeed, but the third invocation causes an ArrayTypeMismatchException to be thrown upon executing the first assignment to array[i]. The exception occurs because a boxed int cannot be stored in a string array.

Array covariance specifically does not extend to arrays of value-types. For example, no conversion exists that permits an int[] to be treated as an object[].


12.6 Array initializers

Array initializers may be specified in field declarations (§10.4), local variable declarations (§8.5.1), and array creation expressions (§7.5.10.2):

array-initializer:
{ variable-initializer-listopt }
{ variable-initializer-list , }
variable-initializer-list:
variable-initializer
variable-initializer-list , variable-initializer
variable-initializer:
expression
array-initializer

An array initializer consists of a sequence of variable initializers, enclosed by “{”and “}” tokens and separated by “,” tokens. Each variable initializer is an expression or, in the case of a multi-dimensional array, a nested array initializer.

The context in which an array initializer is used determines the type of the array being initialized. In an array creation expression, the array type immediately precedes the initializer. In a field or variable declaration, the array type is the type of the field or variable being declared. When an array initializer is used in a field or variable declaration, such as:

int[] a = {0, 2, 4, 6, 8};

it is simply shorthand for an equivalent array creation expression:

int[] a = new int[] {0, 2, 4, 6, 8}

For a single-dimensional array, the array initializer must consist of a sequence of expressions that are assignment compatible with the element type of the array. The expressions initialize array elements in increasing order, starting with the element at index zero. The number of expressions in the array initializer determines the length of the array instance being created. For example, the array initializer above creates an int[] instance of length 5 and then initializes the instance with the following values:

a[0] = 0; a[1] = 2; a[2] = 4; a[3] = 6; a[4] = 8;

For a multi-dimensional array, the array initializer must have as many levels of nesting as there are dimensions in the array. The outermost nesting level corresponds to the leftmost dimension and the innermost nesting level corresponds to the rightmost dimension. The length of each dimension of the array is determined by the number of elements at the corresponding nesting level in the array initializer. For each nested array initializer, the number of elements must be the same as the other array initializers at the same level. The example:

int[,] b = {{0, 1}, {2, 3}, {4, 5}, {6, 7}, {8, 9}};

creates a two-dimensional array with a length of five for the leftmost dimension and a length of two for the rightmost dimension:

int[,] b = new int[5, 2];

and then initializes the array instance with the following values:

b[0, 0] = 0; b[0, 1] = 1;
b[1, 0] = 2; b[1, 1] = 3;
b[2, 0] = 4; b[2, 1] = 5;
b[3, 0] = 6; b[3, 1] = 7;
b[4, 0] = 8; b[4, 1] = 9;

When an array creation expression includes both explicit dimension lengths and an array initializer, the lengths must be constant expressions and the number of elements at each nesting level must match the corresponding dimension length. Some examples:

int i = 3;
int[] x = new int[3] {0, 1, 2};      // OK
int[] y = new int[i] {0, 1, 2};      // Error, i not a constant
int[] z = new int[3] {0, 1, 2, 3};   // Error, length/initializer mismatch

Here, the initializer for y is in error because the dimension length expression is not a constant, and the initializer for z is in error because the length and the number of elements in the initializer do not agree.


13. Interfaces

An interface defines a contract. A class or struct that implements an interface must adhere to its contract. An interface may inherit from multiple base interfaces, and a class or struct may implement multiple interfaces.

Interfaces can contain methods, properties, events, and indexers. The interface itself does not provide implementations for the members that it defines. The interface merely specifies the members that must be supplied by classes or interfaces that implement the interface.


13.1 Interface declarations

An interface-declaration is a type-declaration (§9.5) that declares a new interface type.

interface-declaration:
attributesopt interface-modifiersopt interface identifier interface-baseopt interface-body ;opt

An interface-declaration consists of an optional set of attributes (§17), followed by an optional set of interface-modifiers (§13.1.1), followed by the keyword interface and an identifier that names the interface, optionally followed by an optional interface-base specification (§13.1.2), followed by a interface-body (§13.1.3), optionally followed by a semicolon.


13.1.1 Interface modifiers

An interface-declaration may optionally include a sequence of interface modifiers:

interface-modifiers:
interface-modifier
interface-modifiers interface-modifier
interface-modifier:
new
public
protected
internal
private

It is an error for the same modifier to appear multiple times in an interface declaration.

The new modifier is only permitted on nested interfaces. It specifies that the interface hides an inherited member by the same name, as described in §10.2.2.

The public, protected, internal, and private modifiers control the accessibility of the interface. Depending on the context in which the interface declaration occurs, only some of these modifiers may be permitted (§3.3.1).


13.1.2 Base interfaces

An interface can inherit from zero or more interfaces, which are called the explicit base interfaces of the interface. When an interface has more than zero explicit base interfaces then in the declaration of the interface, the interface identifier is followed by a colon and a comma-separated list of base interface identifiers.

interface-base:
: interface-type-list

The explicit base interfaces of an interface must be at least as accessible as the interface itself (§3.3.4). For example, it is an error to specify a private or internal interface in the interface-base of a public interface.

It is an error for an interface to directly or indirectly inherit from itself.

The base interfaces of an interface are the explicit base interfaces and their base interfaces. In other words, the set of base interfaces is the complete transitive closure of the explicit base interfaces, their explicit base interfaces, and so on. In the example

interface IControl
{
   void Paint();
}
interface ITextBox: IControl
{
   void SetText(string text);
}
interface IListBox: IControl
{
   void SetItems(string[] items);
}
interface IComboBox: ITextBox, IListBox {}

the base interfaces of IComboBox are IControl, ITextBox, and IListBox.

An interface inherits all members of its base interfaces. In other words, the IComboBox interface above inherits members SetText and SetItems as well as Paint.

A class or struct that implements an interface also implicitly implements all of the interface’s base interfaces.


13.1.3 Interface body

The interface-body of an interface defines the members of the interface.

interface-body:
{ interface-member-declarationsopt }


13.2 Interface members

The members of an interface are the members inherited from the base interfaces and the members declared by the interface itself.

interface-member-declarations:
interface-member-declaration
interface-member-declarations interface-member-declaration
interface-member-declaration:
interface-method-declaration
interface-property-declaration
interface-event-declaration
interface-indexer-declaration

An interface declaration may declare zero or more members. The members of an interface must be methods, properties, events, or indexers. An interface cannot contain constants, fields, operators, constructors, destructors, static constructors, or types, nor can an interface contain static members of any kind.

All interface members implicitly have public access. It is an error for interface member declarations to include any modifiers. In particular, interface members cannot be declared with the abstract, public, protected, internal, private, virtual, override, or static modifiers.

The example

public delegate void StringListEvent(IStringList sender);
public interface IStringList
{
   void Add(string s);
   int Count { get; }
   event StringListEvent Changed;
   string this[int index] { get; set; }
}

declares an interface that contains one each of the possible kinds of members: A method, a property, an event, and an indexer.

An interface-declaration creates a new declaration space (§3.1), and the interface-member-declarations immediately contained by the interface-declaration introduce new members into this declaration space. The following rules apply to interface-member-declarations:

  • The name of a method must differ from the names of all properties and events declared in the same interface. In addition, the signature (§3.4) of a method must differ from the signatures of all other methods declared in the same interface.
  • The name of a property or event must differ from the names of all other members declared in the same interface.
  • The signature of an indexer must differ from the signatures of all other indexers declared in the same interface.

The inherited members of an interface are specifically not part of the declaration space of the interface. Thus, an interface is allowed to declare a member with the same name or signature as an inherited member. When this occurs, the derived interface member is said to hide the base interface member. Hiding an inherited member is not considered an error, but it does cause the compiler to issue a warning. To suppress the warning, the declaration of the derived interface member must include a new modifier to indicate that the derived member is intended to hide the base member. This topic is discussed further in §3.5.1.2.

If a new modifier is included in a declaration that doesn’t hide an inherited member, a warning is issued to that effect. This warning is suppressed by removing the new modifier.


13.2.1 Interface methods

Interface methods are declared using interface-method-declarations:

interface-method-declaration:
attributesopt newopt return-type identifier ( formal-parameter-listopt ) ;

The attributes, return-type, identifier, and formal-parameter-list of an interface method declaration have the same meaning as those of a method declaration in a class (§10.5). An interface method declaration is not permitted to specify a method body, and the declaration therefore always ends with a semicolon.


13.2.2 Interface properties

Interface properties are declared using interface-property-declarations:

interface-property-declaration:
attributesopt newopt type identifier { interface-accessors }
interface-accessors:
attributesopt get ;
attributesopt set ;
attributesopt get ; attributesopt set ;
attributesopt set ; attributesopt get ;

The attributes, type, and identifier of an interface property declaration have the same meaning as those of a property declaration in a class (§10.6).

The accessors of an interface property declaration correspond to the accessors of a class property declaration (§10.6.5), except that the accessor body must always be a semicolon. Thus, the accessors simply indicate whether the property is read-write, read-only, or write-only.


13.2.3 Interface events

Interface events are declared using interface-event-declarations:

interface-event-declaration:
attributesopt newopt event type identifier ;

The attributes, type, and identifier of an interface event declaration have the same meaning as those of an event declaration in a class (§10.7).


13.2.4 Interface indexers

Interface indexers are declared using interface-indexer-declarations:

interface-indexer-declaration:
attributesopt newopt type this [ formal-parameter-list ] { interface-accessors }

The attributes, type, and formal-parameter-list of an interface indexer declaration have the same meaning as those of an indexer declaration in a class (§10.8).

The accessors of an interface indexer declaration correspond to the accessors of a class indexer declaration (§10.8), except that the accessor body must always be a semicolon. Thus, the accessors simply indicate whether the indexer is read-write, read-only, or write-only.


13.2.5 Interface member access

Interface members are accessed through member access (§7.5.4) and indexer access (§7.5.6.2) expressions of the form I.M and I[A], where I is an instance of an interface type, M is a method, property, or event of that interface type, and A is an indexer argument list.

For interfaces that are strictly single-inheritance (each interface in the inheritance chain has exactly zero or one direct base interface), the effects of the member lookup (§7.3), method invocation (§7.5.5.1), and indexer access (§7.5.6.2) rules are exactly the same as for classes and structs: More derived members hide less derived members with the same name or signature. However, for multiple-inheritance interfaces, ambiguities can occur when two or more unrelated base interfaces declare members with the same name or signature. This section shows several examples of such situations. In all cases, explicit casts can be included in the program code to resolve the ambiguities.

In the example

interface IList
{
   int Count { get; set; }
}
interface ICounter
{
   void Count(int i);
}
interface IListCounter: IList, ICounter {}
class C
{
   void Test(IListCounter x) {
      x.Count(1);                  // Error, Count is ambiguous
      x.Count = 1;               // Error, Count is ambiguous
      ((IList)x).Count = 1;      // Ok, invokes IList.Count.set
      ((ICounter)x).Count(1);      // Ok, invokes ICounter.Count
   }
}

the first two statements cause compile-time errors because the member lookup (§7.3) of Count in IListCounter is ambiguous. As illustrated by the example, the ambiguity is resolved by casting x to the appropriate base interface type. Such casts have no run-time costs—they merely consist of viewing the instance as a less derived type at compile-time.

In the example

interface IInteger
{
   void Add(int i);
}
interface IDouble
{
   void Add(double d);
}
interface INumber: IInteger, IDouble {}
class C
{
   void Test(INumber n) {
      n.Add(1);               // Error, both Add methods are applicable
      n.Add(1.0);               // Ok, only IDouble.Add is applicable
      ((IInteger)n).Add(1);   // Ok, only IInteger.Add is a candidate
      ((IDouble)n).Add(1);      // Ok, only IDouble.Add is a candidate
   }
}

the invocation n.Add(1) is ambiguous because a method invocation (§7.5.5.1) requires all overloaded candidate methods to be declared in the same type. However, the invocation n.Add(1.0) is permitted because only IDouble.Add is applicable. When explicit casts are inserted, there is only one candidate method, and thus no ambiguity.

In the example

interface IBase
{
   void F(int i);
}
interface ILeft: IBase
{
   new void F(int i);
}
interface IRight: IBase
{
   void G();
}
interface IDerived: ILeft, IRight {}
class A
{
   void Test(IDerived d) {
      d.F(1);               // Invokes ILeft.F
      ((IBase)d).F(1);      // Invokes IBase.F
      ((ILeft)d).F(1);      // Invokes ILeft.F
      ((IRight)d).F(1);      // Invokes IBase.F
   }
}

the IBase.F member is hidden by the ILeft.F member. The invocation d.F(1) thus selects ILeft.F, even though IBase.F appears to not be hidden in the access path that leads through IRight.

The intuitive rule for hiding in multiple-inheritance interfaces is simply this: If a member is hidden in any access path, it is hidden in all access paths. Because the access path from IDerived to ILeft to IBase hides IBase.F, the member is also hidden in the access path from IDerived to IRight to IBase.


13.3 Fully qualified interface member names

An interface member is sometimes referred to by its fully qualified name. The fully qualified name of an interface member consists of the name of the interface in which the member is declared, followed by a dot, followed by the name of the member. For example, given the declarations

interface IControl
{
   void Paint();
}
interface ITextBox: IControl
{
   void SetText(string text);
}

the fully qualified name of Paint is IControl.Paint and the fully qualified name of SetText is ITextBox.SetText.

Note that the fully qualified name of a member references the interface in which the member is declared. Thus, in the example above, it is not possible to refer to Paint as ITextBox.Paint.

When an interface is part of a namespace, the fully qualified name of an interface member includes the namespace name. For example

namespace System
{
   public interface ICloneable
   {
      object Clone();
   }
}

Here, the fully qualified name of the Clone method is System.ICloneable.Clone.


13.4 Interface implementations

Interfaces may be implemented by classes and structs. To indicate that a class or struct implements an interface, the interface identifier is included in the base class list of the class or struct.

interface ICloneable
{
   object Clone();
}
interface IComparable
{
   int CompareTo(object other);
}
class ListEntry: ICloneable, IComparable
{
   public object Clone() {...}
   public int CompareTo(object other) {...}
}

A class or struct that implements an interface also implicitly implements all of the interface’s base interfaces. This is true even if the class or struct doesn’t explicitly list all base interfaces in the base class list.

interface IControl
{
   void Paint();
}
interface ITextBox: IControl
{
   void SetText(string text);
}
class TextBox: ITextBox
{
   public void Paint() {...}
   public void SetText(string text) {...}
}

Here, class TextBox implements both IControl and ITextBox.


13.4.1 Explicit interface member implementations

For purposes of implementing interfaces, a class or struct may declare explicit interface member implementations. An explicit interface member implementation is a method, property, event, or indexer declaration that references a fully qualified interface member name. For example

interface ICloneable
{
   object Clone();
}
interface IComparable
{
   int CompareTo(object other);
}
class ListEntry: ICloneable, IComparable
{
   object ICloneable.Clone() {...}
   int IComparable.CompareTo(object other) {...}
}

Here, ICloneable.Clone and IComparable.CompareTo are explicit interface member implementations.

It is not possible to access an explicit interface member implementation through its fully qualified name in a method invocation, property access, or indexer access. An explicit interface member implementation can only be accessed through an interface instance, and is in that case referenced simply by its member name.

It is an error for an explicit interface member implementation to include access modifiers, as is it an error to include the abstract, virtual, override, or static modifiers.

Explicit interface member implementations have different accessibility characteristics than other members. Because explicit interface member implementations are never accessible through their fully qualified name in a method invocation or a property access, they are in a sense private. However, since they can be accessed through an interface instance, they are in a sense also public.

Explicit interface member implementations serve two primary purposes:

  • Because explicit interface member implementations are not accessible through class or struct instances, they allow interface implementations to be excluded from the public interface of a class or struct. This is particularly useful when a class or struct implements an internal interface that is of no interest to a consumer of the class or struct.
  • Explicit interface member implementations allow disambiguation of interface members with the same signature. Without explicit interface member implementations it would be impossible for a class or struct to have different implementations of interface members with the same signature and return type, as would it be impossible for a class or struct to have any implementation at all of interface members with the same signature but with different return types.

For an explicit interface member implementation to be valid, the class or struct must name an interface in its base class list that contains a member whose fully qualified name, type, and parameter types exactly match those of the explicit interface member implementation. Thus, in the following class

class Shape: ICloneable
{
   object ICloneable.Clone() {...}
   int IComparable.CompareTo(object other) {...}
}

the declaration of IComparable.CompareTo is invalid because IComparable is not listed in the base class list of Shape and is not a base interface of ICloneable. Likewise, in the declarations

class Shape: ICloneable
{
   object ICloneable.Clone() {...}
}
class Ellipse: Shape
{
   object ICloneable.Clone() {...}
}

the declaration of ICloneable.Clone in Ellipse is in error because ICloneable is not explicitly listed in the base class list of Ellipse.

The fully qualified name of an interface member must reference the interface in which the member was declared. Thus, in the declarations

interface IControl
{
   void Paint();
}
interface ITextBox: IControl
{
   void SetText(string text);
}
class TextBox: ITextBox
{
   void IControl.Paint() {...}
   void ITextBox.SetText(string text) {...}
}

the explicit interface member implementation of Paint must be written as IControl.Paint.


13.4.2 Interface mapping

A class or struct must provide implementations of all members of the interfaces that are listed in the base class list of the class or struct. The process of locating implementations of interface members in an implementing class or struct is known as interface mapping.

Interface mapping for a class or struct C locates an implementation for each member of each interface specified in the base class list of C. The implementation of a particular interface member I.M, where I is the interface in which the member M is declared, is determined by examining each class or struct S, starting with C and repeating for each successive base class of C, until a match is located:

  • If S contains a declaration of an explicit interface member implementation that matches I and M, then this member is the implementation of I.M.
  • Otherwise, if S contains a declaration of a non-static public member that matches M, then this member is the implementation of I.M.

An error occurs if implementations cannot be located for all members of all interfaces specified in the base class list of C. Note that the members of an interface include those members that are inherited from base interfaces.

For purposes of interface mapping, a class member A matches an interface member B when:

  • A and B are methods, and the name, type, and formal parameter lists of A and B are identical.
  • A and B are properties, the name and type of A and B are identical, and A has the same accessors as B (A is permitted to have additional accessors if it is not an explicit interface member implementation).
  • A and B are events, and the name and type of A and B are identical.
  • A and B are indexers, the type and formal parameter lists of A and B are identical, and A has the same accessors as B (A is permitted to have additional accessors if it is not an explicit interface member implementation).

Notable implications of the interface mapping algorithm are:

  • Explicit interface member implementations take precedence over other members in the same class or struct when determining the class or struct member that implements an interface member.
  • Private, protected, and static members do not participate in interface mapping.

In the example

interface ICloneable
{
   object Clone();
}
class C: ICloneable
{
   object ICloneable.Clone() {...}
   public object Clone() {...}
}

the ICloneable.Clone member of C becomes the implementation of Clone in ICloneable because explicit interface member implementations take precedence over other members.

If a class or struct implements two or more interfaces containing a member with the same name, type, and parameter types, it is possible to map each of those interface members onto a single class or struct member. For example

interface IControl
{
   void Paint();
}
interface IForm
{
   void Paint();
}
class Page: IControl, IForm
{
   public void Paint() {...}
}

Here, the Paint methods of both IControl and IForm are mapped onto the Paint method in Page. It is of course also possible to have separate explicit interface member implementations for the two methods.

If a class or struct implements an interface that contains hidden members, then some members must necessarily be implemented through explicit interface member implementations. For example

interface IBase
{
   int P { get; }
}
interface IDerived: IBase
{
   new int P();
}

An implementation of this interface would require at least one explicit interface member implementation, and would take one of the following forms

class C: IDerived
{
   int IBase.P { get {...} }
   int IDerived.P() {...}
}
class C: IDerived
{
   public int P { get {...} }
   int IDerived.P() {...}
}
class C: IDerived
{
   int IBase.P { get {...} }
   public int P() {...}
}

When a class implements multiple interfaces that have the same base interface, there can be only one implementation of the base interface. In the example

interface IControl
{
   void Paint();
}
interface ITextBox: IControl
{
   void SetText(string text);
}
interface IListBox: IControl
{
   void SetItems(string[] items);
}
class ComboBox: IControl, ITextBox, IListBox
{
   void IControl.Paint() {...}
   void ITextBox.SetText(string text) {...}
   void IListBox.SetItems(string[] items) {...}
}

it is not possible to have separate implementations for the IControl named in the base class list, the IControl inherited by ITextBox, and the IControl inherited by IListBox. Indeed, there is no notion of a separate identity for these interfaces. Rather, the implementations of ITextBox and IListBox share the same implementation of IControl, and ComboBox is simply considered to implement three interfaces, IControl, ITextBox, and IListBox.

The members of a base class participate in interface mapping. In the example

interface Interface1
{
   void F();
}
class Class1
{
   public void F() {}
   public void G() {}
}
class Class2: Class1, Interface1
{
   new public void G() {}
}

the method F in Class1 is used in Class2's implementation of Interface1.


13.4.3 Interface implementation inheritance

A class inherits all interface implementations provided by its base classes.

Without explicitly re-implementing an interface, a derived class cannot in any way alter the interface mappings it inherits from its base classes. For example, in the declarations

interface IControl
{
   void Paint();
}
class Control: IControl
{
   public void Paint() {...}
}
class TextBox: Control
{
   new public void Paint() {...}
}

the Paint method in TextBox hides the Paint method in Control, but it does not alter the mapping of Control.Paint onto IControl.Paint, and calls to Paint through class instances and interface instances will have the following effects

Control c = new Control();
TextBox t = new TextBox();
IControl ic = c;
IControl it = t;
c.Paint();         // invokes Control.Paint();
t.Paint();         // invokes TextBox.Paint();
ic.Paint();         // invokes Control.Paint();
it.Paint();         // invokes Control.Paint();

However, when an interface method is mapped onto a virtual method in a class, it is possible for derived classes to override the virtual method and alter the implementation of the interface. For example, rewriting the declarations above to

interface IControl
{
   void Paint();
}
class Control: IControl
{
   public virtual void Paint() {...}
}
class TextBox: Control
{
   public override void Paint() {...}
}

the following effects will now be observed

Control c = new Control();
TextBox t = new TextBox();
IControl ic = c;
IControl it = t;
c.Paint();         // invokes Control.Paint();
t.Paint();         // invokes TextBox.Paint();
ic.Paint();         // invokes Control.Paint();
it.Paint();         // invokes TextBox.Paint();

Since explicit interface member implementations cannot be declared virtual, it is not possible to override an explicit interface member implementation. It is however perfectly valid for an explicit interface member implementation to call another method, and that other method can be declared virtual to allow derived classes to override it. For example

interface IControl
{
   void Paint();
}
class Control: IControl
{
   void IControl.Paint() { PaintControl(); }
   protected virtual void PaintControl() {...}
}
class TextBox: Control
{
   protected override void PaintControl() {...}
}

Here, classes derived from Control can specialize the implementation of IControl.Paint by overriding the PaintControl method.


13.4.4 Interface re-implementation

A class that inherits an interface implementation is permitted to re-implement the interface by including it in the base class list.

A re-implementation of an interface follows exactly the same interface mapping rules as an initial implementation of an interface. Thus, the inherited interface mapping has no effect whatsoever on the interface mapping established for the re-implementation of the interface. For example, in the declarations

interface IControl
{
   void Paint();
}
class Control: IControl
{
   void IControl.Paint() {...}
}
class MyControl: Control, IControl
{
   public void Paint() {}
}

the fact that Control maps IControl.Paint onto Control.IControl.Paint doesn’t affect the re-implementation in MyControl, which maps IControl.Paint onto MyControl.Paint.

Inherited public member declarations and inherited explicit interface member declarations participate in the interface mapping process for re-implemented interfaces. For example

interface IMethods
{
   void F();
   void G();
   void H();
   void I();
}
class Base: IMethods
{
   void IMethods.F() {}
   void IMethods.G() {}
   public void H() {}
   public void I() {}
}
class Derived: Base, IMethods
{
   public void F() {}
   void IMethods.H() {}
}

Here, the implementation of IMethods in Derived maps the interface methods onto Derived.F, Base.IMethods.G, Derived.IMethods.H, and Base.I.

When a class implements an interface, it implicitly also implements all of the interface’s base interfaces. Likewise, a re-implementation of an interface is also implicitly a re-implementation of all of the interface’s base interfaces. For example

interface IBase
{
   void F();
}
interface IDerived: IBase
{
   void G();
}
class C: IDerived
{
   void IBase.F() {...}
   void IDerived.G() {...}
}
class D: C, IDerived
{
   public void F() {...}
   public void G() {...}
}

Here, the re-implementation of IDerived also re-implements IBase, mapping IBase.F onto D.F.


13.4.5 Abstract classes and interfaces

Like a non-abstract class, an abstract class must provide implementations of all members of the interfaces that are listed in the base class list of the class. However, an abstract class is permitted to map interface methods onto abstract methods. For example

interface IMethods
{
   void F();
   void G();
}
abstract class C: IMethods
{
   public abstract void F();
   public abstract void G();
}

Here, the implementation of IMethods maps F and G onto abstract methods, which must be overridden in non-abstract classes that derive from C.

Note that explicit interface member implementations cannot be abstract, but explicit interface member implementations are of course permitted to call abstract methods. For example

interface IMethods
{
   void F();
   void G();
}
abstract class C: IMethods
{
   void IMethods.F() { FF(); }
   void IMethods.G() { GG(); }
   protected abstract void FF();
   protected abstract void GG();
}

Here, non-abstract classes that derive from C would be required to override FF and GG, thus providing the actual implementation of IMethods.


14. Enums

An enum type is a distinct type with named constants. Enum declarations may appear in the same places that class declarations can occur.

The example

enum Color
{
   Red,
   Green,
   Blue
}

declares an enum type named Color with members Red, Green, and Blue.


14.1 Enum declarations

An enum declaration declares a new enum type. An enum declaration begins with the keyword enum, and defines the name, accessibility, underlying type, and members of the enum.

enum-declaration:
attributesopt enum-modifiersopt enum identifier enum-baseopt enum-body ;opt
enum-modifiers:
enum-modifier
enum-modifiers enum-modifier
enum-modifier:
new
public
protected
internal
private
enum-base:
: integral-type
enum-body:
{ enum-member-declarationsopt }
{ enum-member-declarations , }

Each enum type has a corresponding integral type called the underlying type of the enum type. This underlying type must be able to represent all the enumerator values defined in the enumeration. An enum declaration may explicitly declare an underlying type of byte, sbyte, short, ushort, int, uint, long or ulong. Note that char cannot be used as an underlying type. An enum declaration that does not explicitly declare an underlying type has an underlying type of int.

The example

enum Color: long
{
   Red,
   Green,
   Blue
}

declares an enum with an underlying type of long. A developer might choose to use an underlying type of long, as in the example, to enable the use of values that are in the range of long but not in the range of int, or to preserve this option for the future.


14.2 Enum members

The body of an enum type declaration defines zero or more enum members, which are the named constants of the enum type. No two enum members can have the same name.

enum-member-declarations:
enum-member-declaration
enum-member-declarations , enum-member-declaration
enum-member-declaration:
attributesopt identifier
attributesopt identifier = constant-expression

Each enum member has an associated constant value. The type of this value is the underlying type for the containing enum. The constant value for each enum member must be in the range of the underlying type for the enum. The example

enum Color: uint
{
   Red = -1,
   Green = -2,
   Blue = -3
}

is in error because the constant values -1, -2, and –3 are not in the range of the underlying integral type uint.

Multiple enum members may share the same associated value. The example

enum Color
{
   Red,
   Green,
   Blue,

   Max = Blue
}

shows an enum that has two enum members – Blue and Max – that have the same associated value.

The associated value of an enum member is assigned either implicitly or explicitly. If the declaration of the enum member has a constant-expression initializer, the value of that constant expression, implicitly converted to the underlying type of the enum, is the associated value of the enum member. If the declaration of the enum member has no initializer, its associated value is set implicitly, as follows:

  • If the enum member is the first enum member declared in the enum type, its associated value is zero.
  • Otherwise, the associated value of the enum member is obtained by increasing the associated value of the previous enum member by one. This increased value must be within the range of values that can be represented by the underlying type.

The example

using System;
enum Color
{
   Red,
   Green = 10,
   Blue
}
class Test
{
   static void Main() {
      Console.WriteLine(StringFromColor(Color.Red));
      Console.WriteLine(StringFromColor(Color.Green));
      Console.WriteLine(StringFromColor(Color.Blue));
   }
   static string StringFromColor(Color c) {
      switch (c) {
         case Color.Red:
            return String.Format("Red = {0}", (int) c);
         case Color.Green:
            return String.Format("Green = {0}", (int) c);
         case Color.Blue:
            return String.Format("Blue = {0}", (int) c);
         default:
            return "Invalid color";
      }
   }
}

prints out the enum member names and their associated values. The output is:

Red = 0
Blue = 11
Green = 10

for the following reasons:

  • the enum member Red is automatically assigned the value zero (since it has no initializer and is the first enum member);
  • the enum member Green is explicitly given the value 10;
  • and the enum member Blue is automatically assigned the value one greater than the member that textually precedes it.

The associated value of an enum member may not, directly or indirectly, use the value of its own associated enum member. Other than this circularity restriction, enum member initializers may freely refer to other enum member initializers, regardless of their textual position. Within an enum member initializer, values of other enum members are always treated as having the type of their underlying type, so that casts are not necessary when referring to other enum members.

The example

enum Circular
{
   A = B,
   B
}

is invalid because the declarations of A and B are circular. A depends on B explicitly, and B depends on A implicitly.

Enum members are named and scoped in a manner exactly analogous to fields within classes. The scope of an enum member is the body of its containing enum type. Within that scope, enum members can be referred to by their simple name. From all other code, the name of an enum member must be qualified with the name of its enum type. Enum members do not have any declared accessibility—an enum member is accessible if its containing enum type is accessible.


14.3 Enum values and operations

Each enum type defines a distinct type; an explicit enumeration conversion (§6.2.2) is required to convert between an enum type and an integral type, or between two enum types. The set of values that an enum type can take on is not limited by its enum members. In particular, any value of the underlying type of an enum can be cast to the enum type, and is a distinct valid value of that enum type.

Enum members have the type of their containing enum type (except within other enum member initializers: see §14.2). The value of an enum member declared in enum type E with associated value v is (E)v.

The following operators can be used on values of enum types: ==, !=, <, >, <=, >= (§7.9.5), + (§7.7.4), - (§7.7.5), ^, &, | (§7.10.2), ~ (§7.6.4), ++, -- (§7.5.9, §7.6.7), sizeof (§18.4.3).

Every enum type automatically derives from the class System.Enum. Thus, inherited methods and properties of this class can be used on values of an enum type.


15. Delegates

Delegates enable scenarios that other languages – C++, Pascal, Modula, and others – have addressed with function pointers. Unlike C++ function pointers, delegates are fully object oriented; unlike C++ pointers to member functions, delegates encapsulate both an object instance and a method.

A delegate declaration defines a class that extends the class System.Delegate. A delegate instance encapsulates a method – a callable entity. For instance methods, a callable entity consists of an instance and a method on the instance. For static methods, a callable entity consists of just a method. If you have a delegate instance and an appropriate set of arguments, you can invoke the delegate with the arguments.

An interesting and useful property of a delegate is that it does not know or care about the class of the object that it references. Any object will do; all that matters is that the method’s signature matches the delegate’s. This makes delegates perfectly suited for “anonymous” invocation.


15.1 Delegate declarations

A delegate-declaration is a type-declaration (§9.5) that declares a new delegate type.

delegate-declaration:
attributesopt delegate-modifiersopt delegate result-type identifier ( formal-parameter-listopt ) ;
delegate-modifiers:
delegate-modifier
delegate-modifiers delegate-modifier
delegate-modifier:
new
public
protected
internal
private

It is an error for the same modifier to appear multiple times in a delegate declaration.

The new modifier is only permitted on delegates declared within another type. It specifies that the delegate hides an inherited member by the same name, as described in §10.2.2.

The public, protected, internal, and private modifiers control the accessibility of the delegate type. Depending on the context in which the delegate declaration occurs, some of these modifiers may not be permitted (§3.3.1).

The formal-parameter-list identifies the signature of the delegate, and the result-type indicates the return type of the delegate. The signature and return type of the delegate must exactly match the signature and return type of any method that the delegate type encapsulates. Delegate types in C# are name equivalent, not structurally equivalent. Two different delegates types that have the same signature and return type are considered different delegate types.

A delegate type is a class type that is derived from System.Delegate. Delegate types are implicitly sealed: it is not permissible to derive any type from a delegate type. It is also not permissible to derive a non-delegate class type from System.Delegate. Note that System.Delegate is not itself a delegate type, it is a class type that all delegate types derive from.

C# provides special syntax for delegate instantiation and invocation. Except for instantiation, any operation that can be applied to a class or class instance can also be applied to a delegate class or instance. In particular, it is possible to access members of the System.Delegate type via the usual member access syntax.


15.1.1 Combinable delegate types

Delegate types are classified into two kinds: combinable and non-combinable. A combinable delegate type must satisfy the following conditions:

  • The declared return type of the delegate must be void.
  • None of the parameters of the delegate type can be declared as output parameters (§10.5.1.3).

A run-time exception occurs if an attempt is made to combine (§7.7.4) two instances of a non-combinable delegate types unless one or the other is null.


15.2 Delegate instantiation

Although delegates behave in most ways like other classes, C# provides special syntax for instantiating a delegate instance. A delegate-creation-expression (§7.5.10.3) is used to create a new instance of a delegate. The newly created delegate instance then refers to either:

  • The static method referenced in the delegate-creation-expression, or
  • The target object (which cannot be null) and instance method referenced in the delegate-creation-expression, or
  • Another delegate

Once instantiated, delegate instances always refer to the same target object and method.


15.3 Multi-cast delegates

Delegates can be combined using the addition operator (§7.7.4), and one delegate can be removed from another using the subtraction operator (§7.7.5). A delegate instance created by combining two or more (non-null) delegate instances is called a multicast delegate instance. For any delegate instance, the invocation list of the delegate is defined as the ordered list of non-multicast delegates that would be invoked if the delegate instance were invoked. More precisely:

  • For a non-multicast delegate instance, the invocation list consists of the delegate instance itself.
  • For a multi-cast delegate instance that was created by combining two delegates, the invocation list is the formed by concatenating the invocation lists of the two operands of the addition operation that formed the multi-cast delegate.


15.4 Delegate invocation

C# provides special syntax for invoking a delegate. When a non-multicast delegate is invoked, it invokes the method that the delegate refers to with the same arguments, and returns the same value that the referred to method returns. See §7.5.5.2 for detailed information on delegate invocation. If an exception occurs during the invocation of a delegate, and the exception is not caught within the method that was invoked, the search for an exception catch clause continues in the method that called the delegate, as if that method had directly called the method that the delegate referred to.

Invocation of a multi-cast delegate proceeds by invocation each of the delegates on the invocation list, in order. Each call is passed the same set of arguments. If the delegate includes reference parameters (§10.5.1.2), each method invocation will occur with a reference to the same variable; changes to that variable by one method in the invocation list will be “seen” by any later methods in the invocation list.

If an exception occurs during processing of the invocation of a multicast delegate, and the exception is not caught within the method that was invoked, the search for an exception catch clause continues in the method that called the delegate, and any methods later in the invocation list are not invoked.


16. Exceptions

Exceptions in C# provide a structured, uniform, and type-safe way of handling both system level and application level error conditions. The exception mechanism is C# is quite similar to that of C++, with a few important differences:

  • In C#, all exceptions must be represented by an instance of a class type derived from System.Exception. In C++, any value of any type can be used to represent an exception.
  • In C#, a finally block (§8.10) can be used to write termination code that executes in both normal execution and exceptional conditions. Such code is difficult to write in C++ without duplicating code.
  • In C#, system-level exceptions such as overflow, divide-by-zero, and null dereferences have well defined exception classes and are on a par with application-level error conditions.


16.1 Causes of exceptions

Exception can be thrown in two different ways.

  • A throw statement (§8.9.5) throws an exception immediately and unconditionally. Control never reaches the statement immediately following the throw.
  • Certain exceptional conditions that arise during the processing of C# statements and expression cause an exception in certain circumstances when the operation cannot be completed normally. For example, an integer division operation (§7.7.2) throws a System.DivideByZeroException if the denominator is zero. See §16.4 for a list of the various exceptions that can occur in this way.


16.2 The System.Exception class

The System.Exception class is the base type of all exceptions. This class has a few notable properties that all exceptions share:

  • Message is a read-only property that contains a human-readable description of the reason for the exception.
  • InnerException is a read-only property that contains the “inner exception” for this exception. If this is not null, this indicates that the current exception was thrown in response to another exception. The exception that caused the current exception is available in the InnerException property.

The value of these properties can be specified in the constructor for System.Exception.


16.3 How exceptions are handled

Exception are handled by a try statement (§8.10).

When an exception occurs, the system searches for the nearest catch clause than can handle the exception, as determined by the run-time type of the exception. First, the current method is searched for a lexically enclosing try statement, and the associated catch clauses of the try statement are considered in order. If that fails, the method that called try statement, and the current method is searched for a lexically enclosing try statement that encloses the point of the call to the current method. This search continues until a catch clause is found that can handle the current exception, by naming an exception class that is of the same class, or a base class, of the run-time type of the exception being thrown. A catch clause that doesn’t name an exception class can handle any exception.

Once a matching catch clause is found, the system transfers control to the first statement of the catch clause. Before execution of the catch clause begins, the system first executes in order any finally clauses that were associated with try statements more nested that than the one that caught the exception.

If no matching catch clauses is no found, one of two things occurs:

  • If the search for a matching catch clause reaches a static constructor (§10.12) or static field initializer, then a System.TypeInitializationException is thrown at the point that triggered the invocation of the static constructor. The inner exception of the TypeInitializationException contains the exception that was originally thrown.
  • If the search for matching catch clauses reaches the code that initially started the thread or program, then execution of the thread or program is terminated.


16.4 Common Exception Classes

The following exceptions are thrown by certain C# operations.

System.OutOfMemoryException | Thrown when an attempt to allocate memory (via new) fails.
System.StackOverflowException | Thrown when the execution stack is exhausted by having too many pending method calls; typically indicative of very deep or unbounded recursion.
System.NullReferenceException | Thrown when a null reference is used in a way that causes the referenced object to be required.
System.TypeInitializationException | Thrown when a static constructor throws an exception, and no catch clauses exists to catch in.
System.InvalidCastException | Thrown when an explicit conversion from a base type or interface to a derived types fails at run time.
System.ArrayTypeMismatchException | Thrown when a store into an array fails because the actual type of the stored element is incompatible with the actual type of the array.
System.IndexOutOfRangeException | Thrown when an attempt to index an array via an index that is less than zero or outside the bounds of the array.
System.MulticastNotSupportedException | Thrown when an attempt to combine two non-null delegates fails, because the delegate type does not have a void return type.
System.ArithmeticException | A base class for exceptions that occur during arithmetic operations, such as DivideByZeroException and OverflowException.
System.DivideByZeroException | Thrown when an attempt to divide an integral value by zero occurs.
System.OverflowException | Thrown when an arithmetic operation in a checked context overflows.


17. Attributes

Much of the C# language enables the programmer to specify declarative information about the entities defined in the program. For example, the accessibility of a method in a class is specified by decorating it with the method-modifiers public, protected, internal, and private.

C# enables programmers to invent new kinds of declarative information, to specify declarative information for various program entities, and to retrieve attribute information in a run-time environment. For instance, a framework might define a HelpAttribute attribute that can be placed on program elements such as classes and methods to provide a mapping from program elements to documentation for them.

New kinds of declarative information are defined through the declaration of attribute classes (§17.1), which may have positional and named parameters (§17.1.2). Declarative information is specified a C# program using attributes (§17.2), and can be retrieved at run-time as attribute instances (§17.3).


17.1 Attribute classes

The declaration of an attribute class defines a new kind of attribute that can be placed on a declaration. A class that derives from the abstract class System.Attribute, whether directly or indirectly, is an attribute class. By convention, attribute classes are named with a suffix of Attribute. Uses of an attribute may either include or omit this suffix.


17.1.1 The AttributeUsage attribute

The AttributeUsage attribute is used to describe how an attribute class can be used.

The AttributeUsage attribute has a positional parameter named that enables an attribute class to specify the kinds of declarations on which it can be used. The example

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)]
public class SimpleAttribute: System.Attribute
{}

defines an attribute class named SimpleAttribute that can be placed on class-declarations and interface-declarations. The example

[Simple] class Class1 {…}
[Simple] interface Interface1 {…}

shows several uses of the Simple attribute. The attribute is defined with a class named SimpleAttribute, but uses of this attribute may omit the Attribute suffix, thus shortening the name to Simple. The example above is semantically equivalent to the example

[SimpleAttribute] class Class1 {…}
[SimpleAttribute] interface Interface1 {…}

The AttributeUsage attribute has an AllowMultiple named parameter that specifies whether the indicated attribute can be specified more than once for a given entity. An attribute that can be specified more than once on an entity is called a multi-use attribute class. An attribute that can be specified at most once on an entity is called a single-use attribute class.

The example

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class AuthorAttribute: System.Attribute {
   public AuthorAttribute(string value);
   public string Value { get {…} }
}

defines a multi-use attribute class named AuthorAttribute. The example

[Author("Brian Kernighan"), Author("Dennis Ritchie")]
class Class1 {…}

shows a class declaration with two uses of the Author attribute.


17.1.2 Positional and named parameters

Attribute classes can have positional parameters and named parameters. Each public constructor for an attribute class defines a valid sequence of positional parameters for the attribute class. Each non-static public read-write field and property for an attribute class defines a named parameter for the attribute class.

The example

[AttributeUsage(AttributeTargets.Class)]
public class HelpAttribute: System.Attribute
{
   public HelpAttribute(string url) {   // url is a positional parameter
      …
   }
   public string Topic {   // Topic is a named parameter
      get {...}
      set {...}
   }
   public string Url { get {…} }
}

defines an attribute class named HelpAttribute that has one positional parameter (string url) and one named argument (string Topic). The read-only Url property does not define a named parameter. It is non-static and public, but since it is read-only it does not define a named parameter.

The example

[HelpAttribute("http://www.mycompany.com/…/Class1.htm")]
class Class1 {
}
[HelpAttribute("http://www.mycompany.com/…/Misc.htm", Topic ="Class2")]
class Class2 {
}

shows several uses of the attribute.


17.1.3 Attribute parameter types

By convention, the types of positional and named parameters for an attribute class are limited to the attribute parameter types. A type is an attribute type if it is one of the following:

  • One of the following types: bool, byte, char, double, float, int, long, short, string.
  • The type object.
  • The type System.Type.
  • An enum type provided that it has public accessibility and that the types in which it is nested (if any) also have public accessibility.


17.2 Attribute specification

An attribute is a piece of additional declarative information that is specified for a declaration. Attributes can be specified for type-declarations, class-member-declarations, interface-member-declarations, enum-member-declarations, property-accessor-declarations and formal-parameter declarations.

Attributes are specified in attribute sections. Each attribute section is surrounded in square brackets, with multiple attributes specified in a comma-separated lists. The order in which attributes are specified, and the manner in which they are arranged in sections is not significant. The attribute specifications [A][B], [B][A], [A, B], and [B, A] are equivalent.

attributes:
attribute-sections
attribute-sections:
attribute-section
attribute-sections attribute-section
attribute-section:
[ attribute-list ]
[ attribute-list ,]
attribute-list:
attribute
attribute-list , attribute
attribute:
attribute-name attribute-argumentsopt
attribute-name:
reserved-attribute-name
type-name
attribute-arguments:
( positional-argument-list )
( positional-argument-list , named-argument-list )
( named-argument-list )
positional-argument-list:
positional-argument
positional-argument-list , positional-argument
positional-argument:
attribute-argument-expression
named-argument-list:
named-argument
named-argument-list , named-argument
named-argument:
identifier = attribute-argument-expression
attribute-argument-expression:
expression

An attribute consists of an attribute-name and an optional list of positional and named arguments. The positional arguments (if any) precede the named arguments. A positional argument consists of an attribute-argument-expression; a named argument consists of a name, followed by an equal sign, followed by an attribute-argument-expression.

The attribute-name identifies either a reserved attribute or an attribute class. If the form of attribute-name is type-name then this name must refer to an attribute class. Otherwise, a compile-time error occurs. The example

class Class1 {}
[Class1] class Class2 {}   // Error

is in error because it attempts to use Class1, which is not an attribute class, as an attribute class.

By convention, attribute classes are named with a suffix of Attribute. An attribute-name of the form type-name may either include or omit this suffix. An exact match between the attribute-name and the name of the attribute class is preferred. The example

[AttributeUsage(AttributeTargets.All)]
public class X: System.Attribute
{}
[AttributeUsage(AttributeTargets.All)]
public class XAttribute: System.Attribute
{}
[X]               // refers to X
class Class1 {}
[XAttribute]      // refers to XAttribute
class Class2 {}

shows two attribute classes named X and XAttribute. The attribute [X] refers to the class named X, and the attribute [XAttribute] refers to the attribute class named [XAttribute]. If the declaration for class X is removed, then both attributes refer to the attribute class named XAttribute:

[AttributeUsage(AttributeTargets.All)]
public class XAttribute: System.Attribute
{}
[X]               // refers to XAttribute
class Class1 {}
[XAttribute]      // refers to XAttribute
class Class2 {}

It is an error to use a single-use attribute class more than once on the same entity. The example

[AttributeUsage(AttributeTargets.Class)]
public class HelpStringAttribute: System.Attribute
{
   string value;
   public HelpStringAttribute(string value) {
      this.value = value;
   }
   public string Value { get {…} }
}
[HelpString("Description of Class1")]
[HelpString("Another description of Class1")]
public class Class1 {}

is in error because it attempts to use HelpString, which is a single-use attribute class, more than once on the declaration of Class1.

An expression E is an attribute-argument-expression if all of the following statements are true:

  • The type of E is an attribute parameter type (§17.1.3).
  • At compile-time, the value of E can be resolved to one of the following:
    • A constant value.
    • A System.Type object.
    • A one-dimensional array of attribute-argument-expressions.


17.3 Attribute instances

An attribute instance is an instance that represents an attribute at run-time. An attribute is defined with an attribute, positional arguments, and named arguments. An attribute instance is an instance of the attribute class that is initialized with the positional and named arguments.

Retrieval of an attribute instance involves both compile-time and run-time processing, as described in the following sections.


17.3.1 Compilation of an attribute

The compilation of an attribute with attribute class T, positional-argument-list P and named-argument-list N, consists of the following steps:

  • Follow the compile-time processing steps for compiling an object-creation-expression of the form new T(P). These steps either result in a compile-time error, or determine a constructor on T that can be invoked at run-time. Call this constructor C.
  • If the constructor determined in the step above does not have public accessibility, then a compile-time error occurs.
  • For each named-argument Arg in N:
    • Let Name be the identifier of the named-argument Arg.
    • Name must identify a non-static read-write public field or property on T. If T has no such field or property, then a compile-time error occurs.
  • Keep the following information for run-time instantiation of the attribute instance: the attribute class T, the constructor C on T, the positional-argument-list P and the named-argument-list N.


17.3.2 Run-time retrieval of an attribute instance

Compilation of an attribute yields an attribute class T, constructor C on T, positional-argument-list P and named-argument-list N. Given this information, an attribute instance can be retrieved at run-time using the following steps:

  • Follow the run-time processing steps for executing an object-creation-expression of the form T(P), using the constructor C as determined at compile-time. These steps either result in an exception, or produce an instance of T. Call this instance O.
  • For each named-argument Arg in N, in order:
    • Let Name be the identifier of the named-argument Arg. If Name does not identify a non-static public read-write field or property on O, then an exception is thrown.
    • Let Value be the result of evaluating the attribute-argument-expression of Arg.
    • If Name identifies a field on O, then set this field to the value Value.
    • Otherwise, Name identifies a property on O. Set this property to the value Value.
    • The result is O, an instance of the attribute class T that has been initialized with the positional-argument-list P and the named-argument-list N.


17.4 Reserved attributes

A small number of attributes affect the language in some way. These attributes include:

  • System.AttributeUsageAttribute, which is used to describe the ways in which an attribute class can be used.
  • System.ConditionalAttribute, which is used to define conditional methods.
  • System.ObsoleteAttribute, which is used to mark a member as obsolete.


17.4.1 The AttributeUsage attribute

The AttributeUsage attribute is used to describe the manner in which the attribute class can be used.

A class that is decorated with the AttributeUsage attribute must derive from System.Attribute, either directly or indirectly. Otherwise, a compile-time error occurs.

[AttributeUsage(AttributeTargets.Class)]
public class AttributeUsageAttribute: System.Attribute
{
   public AttributeUsageAttribute(AttributeTargets validOn) {…}
   public AttributeUsageAttribute(AttributeTargets validOn,
                                    bool allowMultiple,
                                    bool inherited) {…}
   public virtual bool AllowMultiple { get {…} set {…} }
   public virtual bool Inherited { get {…} set {…} }
   public virtual AttributeTargets ValidOn { get {…} }
}
public enum AttributeTargets
{
   Assembly    = 0x0001,
   Module       = 0x0002,
   Class       = 0x0004,
   Struct       = 0x0008,
   Enum          = 0x0010,
   Constructor = 0x0020,
   Method       = 0x0040,
   Property    = 0x0080,
   Field       = 0x0100,
   Event       = 0x0200,
   Interface    = 0x0400,
   Parameter    = 0x0800,
   Delegate    = 0x1000,
   All = Assembly | Module | Class | Struct | Enum | Constructor |
         Method | Property | Field | Event | Interface | Parameter |
         Delegate,
   ClassMembers  =  Class | Struct | Enum | Constructor | Method |
                  Property | Field | Event | Delegate | Interface,
}


17.4.2 The Conditional attribute

The Conditional attribute enables the definition of conditional methods. The Conditional attribute indicates a condition in the form of a pre-processing identifier. Calls to a conditional method are either included or omitted depending on whether this symbol is defined at the point of the call. If the symbol is defined, then the method call is included if the symbol is undefined, then the call is omitted.

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class ConditionalAttribute: System.Attribute
{
   public ConditionalAttribute(string conditionalSymbol) {…}
   public string ConditionalSymbol { get {…} }
}

A conditional method is subject to the following restrictions:

  • The conditional method must be a method in a class-declaration. A compile-time error occurs if the Conditional attribute is specified on an interface method.
  • The conditional method must return have a return type of void.
  • The conditional method must not be marked with the override modifier. A conditional method may be marked with the virtual modifier. Overrides of such a method are implicitly conditional, and must not be explicitly marked with a Conditional attribute.
  • The conditional method must not be an implementation of an interface method. Otherwise, a compile-time error occurs.

Also, a compile-time error occurs if a conditional method is used in a delegate-creation-expression. The example

#define DEBUG
class Class1
{
   [Conditional("DEBUG")]
   public static void M() {
      Console.WriteLine("Executed Class1.M");
   }
}
class Class2
{
   public static void Test() {
      Class1.M();
   }
}

declares Class1.M as a conditional method. Class2's Test method calls this method. Since the pre-processing symbol DEBUG is defined, if Class2.Test is called, it will call M. If the symbol DEBUG had not been defined, then Class2.Test would not call Class1.M.

It is important to note that the inclusion or exclusion of a call to a conditional method is controlled by the pre-processing identifiers at the point of the call. In the example

// Begin class1.cs
   class Class1
   {
      [Conditional("DEBUG")]
      public static void F() {
         Console.WriteLine("Executed Class1.F");
      }
   }
// End class1.cs

// Begin class2.cs
   #define DEBUG
   class Class2
   {
      public static void G {
         Class1.F();            // F is called
      }
   }
// End class2.cs

// Begin class3.cs
   #undef DEBUG
   class Class3
   {
      public static void H {
         Class1.F();            // F is not called
      }
   }
// End class3.cs

the classes Class2 and Class3 each contain calls to the conditional method Class1.F, which is conditional based on the presence or absence of DEBUG. Since this symbol is defined in the context of Class2 but not Class3, the call to F in Class2 is actually made, while the call to F in Class3 is omitted.

The use of conditional methods in an inheritance chain can be confusing. Calls made to a conditional method through base, of the form base.M, are subject to the normal conditional method call rules. In the example

// Begin class1.cs
   class Class1
   {
      [Conditional("DEBUG")]
      public virtual void M() {
         Console.WriteLine("Class1.M executed");
      }
   }
// End class1.cs

// Begin class2.cs
   class Class2: Class1
   {
      public override void M() {
         Console.WriteLine("Class2.M executed");
         base.M();                  // base.M is not called!
      }
   }
// End class2.cs

// Begin class3.cs
   #define DEBUG
   class Class3
   {
      public static void Test() {
         Class2 c = new Class2();
         c.M();                     // M is called
      }
   }
// End class3.cs

Class2 includes a call the M defined in its base class. This call is omitted because the base method is conditional based on the presence of the symbol DEBUG, which is undefined. Thus, the method writes to the console only "Class2.M executed". Judicious use of pp-declarations can eliminate such problems.


17.4.3 The Obsolete attribute

The Obsolete attribute is used to mark program elements that should no longer be used.

[AttributeUsage(AttributeTargets.All)]
public class ObsoleteAttribute: System.Attribute
{
   public ObsoleteAttribute(string message) {…}
   public string Message { get {…} }
   public bool IsError{ get {…} set {…} }
}


18. Unsafe code

© 1987-2000 Microsoft Corporation. All rights reserved.


18.1 Unsafe code

C# Language Reference

18.1 Unsafe contexts

© 1987-2000 Microsoft Corporation. All rights reserved.


18.2 Pointer types

type:
value-type
reference-type
pointer-type
pointer-type:
unmanaged-type *
void *
unmanaged-type:
type


18.2.1 Unmanaged types

© 1987-2000 Microsoft Corporation. All rights reserved.


18.2.2 Pointer values

© 1987-2000 Microsoft Corporation. All rights reserved.


18.2.3 Fixed and moveable variables

© 1987-2000 Microsoft Corporation. All rights reserved.


18.3 Pointer conversions

© 1987-2000 Microsoft Corporation. All rights reserved.


18.3.1 Implicit pointer conversions

© 1987-2000 Microsoft Corporation. All rights reserved.


18.3.2 Explicit pointer conversions

© 1987-2000 Microsoft Corporation. All rights reserved.


18.4 Pointer operations

primary-expression:
...
pointer-member-access
pointer-element-access
sizeof-expression
stackalloc-expression
unary-expression:
...
pointer-indirection-expression
addressof-expression


18.4.1 Pointer member access

pointer-member-access:
primary-expression -> identifier


18.4.2 Pointer element access

pointer-element-access:
primary-expression [ expression ]


18.4.3 The sizeof operator

sizeof-expression:
sizeof ( type )


18.4.4 The stackalloc operator

stackalloc-expression:
stackalloc type [ expression ]


18.4.5 Pointer indirection

pointer-indirection-expression:
* unary-expression


18.4.6 The address-of operator

addressof-expression:
& unary-expression


18.4.7 Pointer increment and decrement

© 1987-2000 Microsoft Corporation. All rights reserved.


18.4.8 Pointer arithmetic

© 1987-2000 Microsoft Corporation. All rights reserved.


18.4.9 Pointer comparison

© 1987-2000 Microsoft Corporation. All rights reserved.


18.5 The fixed statement

fixed-statement:
fixed ( pointer-type fixed-pointer-declarators ) embedded-statement
fixed-pointer-declarators:
fixed-pointer-declarator
fixed-pointer-declarators , fixed-pointer-declarator
fixed-pointer-declarator:
identifier = & variable-reference
identifier = expression


19. Interoperability

The attributes described in this chapter are used for creating programs that interoperate with COM programs.


19.1 The COMImport attribute

When placed on a class, the COMImport attribute marks the class as an externally implemented COM class. Such a class declaration enables the use of a C# name to refer to a COM class.

namespace System.Runtime.InteropServices
{
   [AttributeUsage(AttributeTargets.Class)]
   public class COMImportAttribute: System.Attribute
   {
      public COMImportAttribute() {…}
   }
}

A class that is decorated with the COMImport attribute is subject to the following restrictions:

  • It must also be decorated with the Guid attribute, which specifies the CLSID for the COM class being imported. A compile-time error occurs if a class declaration includes the COMImport attribute but fails to include the Guid attribute.
  • It must not have any members. (A public constructor with no parameters is automatically provided.)
  • It must not derive from a class other than object.

The example

using System.Runtime.InteropServices;
[COMImport, Guid("00020810-0000-0000-C000-000000000046")]
class Worksheet {}
class Test
{
   static void Main() {
      Worksheet w = new Worksheet();   // Creates an Excel worksheet
   }
}

declares a class Worksheet as a class imported from COM that has a CLSID of "00020810-0000-0000-C000-000000000046". Instantiating a Worksheet instance causes a corresponding COM instantiation.


19.2 The COMRegisterFunction attribute

The presence of the COMRegisterFunction attribute on a method indicates that the method should be called during the COM registration process.

namespace System.Runtime.InteropServices
{
   [AttributeUsage(AttributeTargets.Method)]
   public class COMRegisterFunctionAttribute: System.Attribute
   {
      public ComRegisterFunctionAttribute() {…}
   }
}


19.3 The COMSourceInterfaces attribute

The COMSourceInterfaces attribute is used to list the source interfaces on the imported coclass.

namespace System.Runtime.InteropServices
{
   [AttributeUsage(AttributeTargets.Class)]
   public class ComSourceInterfacesAttribute: System.Attribute
   {
      public ComSourceInterfacesAttribute(string value) {…}
      public string Value { get {…} }
   }
}


19.4 The COMVisible attribute

The COMVisible attribute is used to specify whether or not a class or interface is visible in COM.

namespace System.Runtime.InteropServices
{
   [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)]
   public class COMVisibleAttribute: System.Attribute
   {
      public COMVisibleAttribute(bool value) {…}
      public bool Value { get {…} }
   }
}


19.5 The DispId attribute

The DispId attribute is used to specify an OLE Automation DISPID. A DISPID is an integral value that identifies a member in a dispinterface.

namespace System.Runtime.InteropServices
{
   [AttributeUsage(AttributeTargets.Method | AttributeTargets.Field |    AttributeTargets.Property)]
   public class DispIdAttribute: System.Attribute
   {
      public DispIdAttribute(int value) {…}
      public int Value { get {…} }
   }
}


19.6 The DllImport attribute

The DllImport attribute is used to specify the dll location that contains the implementation of an extern method.

namespace System.Runtime.InteropServices
{
   [AttributeUsage(AttributeTargets.Method)]
   public class DllImportAttribute: System.Attribute
   {
      public DllImportAttribute(string dllName) {…}
      public CallingConvention CallingConvention;
      public CharSet CharSet;
      public string EntryPoint;
      public bool ExactSpelling;
      public bool SetLastError;
      public bool TransformSig;
      public string Value { get {…} }
   }
}

Specifically, the DllImport attribute has the following behaviors:

  • It can only be placed on method declarations.
  • It has a single positional parameter: a dllName parameter that specifies name of the dll in which the imported method can be found.
  • It has five named parameters:
    • The CallingConvention parameter indicates the calling convention for the entry point. If no CallingConvention is specified, a default of CallingConvention.Winapi is used.
    • The CharSet parameter indicates the character set used in the entry point. If no CharSet is specified, a default of CharSet.Auto is used.
    • The EntryPoint parameter gives the name of the entry point in the dll. If no EntryPoint is specified, then the name of the method itself is used.
    • The ExactSpelling parameter indicates whether EntryPoint must exactly match the spelling of the indicated entry point. If no ExactSpelling is specified, a default of false is used.
    • The SetLastError parameter indicates whether the method preserves the Win32 "last error". If no SetLastError is specified, a default of false is used.
    • The TransformSig parameter indicates whether the signature of the method should be transformed to one having an HRESULT return value and an additional out parameter named retval for the return value. If no TransformSig value is specified, a default of false is used.
  • It is a single-use attribute class.

In addition, a method that is decorated with the DllImport attribute must have the extern modifier.


19.7 The FieldOffset attribute

The FieldOffset attribute is used to specify the layout of fields for the struct.

namespace System.Runtime.InteropServices
{
   [AttributeUsage(AttributeTargets.Field)]
   public class FieldOffsetAttribute: System.Attribute
   {
      public FieldOffsetAttribute(int value) {…}
      public int Value { get {…} }
   }
}

The FieldOffset attribute may not be placed on a field declarations that is a member of a class.


19.8 The GlobalObject attribute

The presence of the GlobalObject attribute specifies that a class is a "global" or "appobject" class in COM.

namespace System.Runtime.InteropServices
{
   [AttributeUsage(AttributeTargets.Class)]
   public class GlobalObjectAttribute: System.Attribute
   {
      public GlobalObjectAttribute() {…}
   }
}


19.9 The Guid attribute

The Guid attribute is used to specify a globally unique identifier (GUID) for a class or an interface. This information is primarily useful for interoperability with COM.

namespace System.Runtime.InteropServices
{
   [AttributeUsage(AttributeTargets.Class
                  | AttributeTargets.Interface
                  | AttributeTargets.Enum
                  | AttributeTargets.Delegate
                  | AttributeTargets.Struct)]
   public class GuidAttribute: System.Attribute
   {
      public GuidAttribute(string value) {…}
      public string Value { get {…} }
   }
}

The format of the positional string argument is verified at compile-time. It is an error to specify a string argument that is not a syntactically valid GUID.


19.10 The HasDefaultInterface attribute

If present, the HasDefaultInterface attribute indicates that a class has a default interface.

namespace System.Runtime.InteropServices
{
   [AttributeUsage(AttributeTargets.Class)]
   public class HasDefaultInterfaceAttribute: System.Attribute
   {
      public HasDefaultInterfaceAttribute() {…}
   }
}


19.11 The ImportedFromTypeLib attribute

The ImportedFromTypeLib attribute is used to specify that a module was imported from a COM type library.

namespace System.Runtime.InteropServices
{
   [AttributeUsage(AttributeTargets.Module)]
   public class ImportedFromTypeLib: System.Attribute
   {
      public ImportedFromTypeLib(string value) {…}
      public string Value { get {..} }
   }
}


19.12 The In and Out attributes

The In and Out attributes are used to provide custom marshalling information for parameters. All combinations of these marshalling attributes are permitted.

namespace System.Runtime.InteropServices
{
   [AttributeUsage(AttributeTargets.Parameter)]
   public class InAttribute: System.Attribute
   {
      public InAttribute() {…}
   }
   [AttributeUsage(AttributeTargets.Parameter)]
   public class OutAttribute: System.Attribute
   {
      public OutAttribute() {…}
   }
}

If a parameter is not decorated with either marshalling attribute, then it is marshalled based on the its parameter-modifiers, as follows. If the parameter has no modifiers then the marshalling is [In]. If the parameter has the ref modifier then the marshalling is [In, Out]. If the parameter has the out modifier then the marshalling is [Out].

Note that out is a keyword, and Out is an attribute. The example

class Class1
{
   void M([Out] out int i) {
      …
   }
}

shows that the use of out as a parameter-modifier and the use of Out in an attribute.


19.13 The InterfaceType attribute

When placed on an interface, the InterfaceType attribute specifies the manner in which the interface is treated in COM.

namespace System.Runtime.InteropServices
{
   [AttributeUsage(AttributeTargets.Interface)]
   public class InterfaceTypeAttribute: System.Attribute
   {
      public InterfaceTypeAttribute(ComInterfaceType value) {…}
      public ComInterfaceType Value { get {…} }
   }
}


19.14 The MarshalAs attribute

The MarshalAs attribute is used to describe the marshalling format for a field, method, or parameter.

namespace System.Runtime.InteropServices
{
   [AttributeUsage(AttributeTargets.Method |
               AttributeTargets.Parameter |
               AttributeTargets.Field)]
   public class MarshalAsAttribute: System.Attribute
   {
      public MarshalAsAttribute(UnmanagedType unmanagedType) {…}
      public UnmanagedType ArraySubType;
      public string MarshalCookie;
      public string MarshalType;
      public string MarshalTypeLibGuid;
      public string MarshalUnmanagedType;
      public VarEnum SafeArraySubType;
      public int SizeConst;
      public short SizeParamIndex;
      public int SizeParamMultiplier;
   }
}


19.15 The NoIDispatch attribute

The presence of the NoIDispatch attribute indicates that the class or interface should derive from IUnknown rather than IDispatch when exported to COM.

namespace System.Runtime.InteropServices
{
   [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)]
   public class NoIDispatchAttribute: System.Attribute
   {
      public NoIDispatchAttribute() {…}
   }
}


19.16 The NonSerialized attribute

The presence of the NonSerialized attribute on a field or property indicates that that field or property should not be serialized.

namespace System
{
   [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
   public class NonSerializedAttribute: Attribute
   {
      public NonSerializedAttribute() {…}
   }
}


19.17 The Predeclared attribute

The presence of the Predeclared attribute denotes a predeclared object imported from COM.

namespace System.Runtime.InteropServices
{
   [AttributeUsage(Attribute(AttributeTargets.Class)]
   public class PredeclaredAttribute: System.Attribute
   {
      public PredeclaredAttribute() {…}
   }
}


19.18 The PreserveSig attribute

The PreserveSig attribute is used to mark a method as returning an HRESULT result in COM .

namespace System.Runtime.InteropServices
{
   [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)]
   public class PreserveSigAttribute: System.Attribute
   {
      public PreserveSigAttribute(bool value) {…}
      public bool Value { get {…} }
   }
}

The PreserveSig attribute is used to indicate that the HRESULT/retval signature transformation that normally takes place during interoperability calls should be suppressed.


19.19 The Serializable attribute

The presence of the Serializable attribute on a class indicates that the class can be serialized.

namespace System
{
   [AttributeUsage(AttributeTargets.Class
                     | AttributeTargets.Delegate
                     | AttributeTargets.Enum
                     | AttributeTargets.Struct)]
   public class SerializableAttribute: System.Attribute
   {
      public SerializableAttribute() {…}
   }
}


19.20 The StructLayout attribute

The StructLayout attribute is used to specify the layout of fields for the struct.

namespace System.Runtime.InteropServices
{
   [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
   public class StructLayoutAttribute: System.Attribute
   {
       public StructLayoutAttribute(LayoutKind value) {…}
       public CharSet CharSet;
       public bool CheckFastMarshal;
       public int Pack;
       public LayoutKind Value { get {…} }
   }
}

If LayoutKind.Explicit is specified, then every field in the struct must have the StructOffset attribute. If LayoutKind.Explicit is not specified, then use of the StructOffset attribute is prohibited.


19.21 The TypeLibFunc attribute

The TypeLibFunc attribute is used to specify typelib flags, for interoperability with COM.

namespace System.Runtime.InteropServices
{
   [AttributeUsage(AttributeTargets.Method)]
   public class TypeLibFuncAttribute: System.Attribute
   {
      public TypeLibFuncAttribute(TypeLibFuncFlags value) {…}
      public TypeLibFuncFlags Value { get {…} }
   }
}


19.22 The TypeLibType attribute

The TypeLibType attribute is used to specify typelib flags, for interoperability with COM.

namespace System.Runtime.InteropServices
{
   [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)]
   public class TypeLibTypeAttribute: System.Attribute
   {
      public TypeLibTypeAttribute(TypeLibTypeFlags value) {…}
      public TypeLibTypeFlags Value { get {…} }
   }
}


19.23 The TypeLibVar attribute

The TypeLibVar attribute is used to specify typelib flags, for interoperability with COM.

namespace System.Runtime.InteropServices
{
   [AttributeUsage(AttributeTargets.Field)]
   public class TypeLibVarAttribute: System.Attribute
   {
      public TypeLibVarAttribute(TypeLibVarFlags value) {…}
      public TypeLibVarFlags Value { get {…} }
   }
}


19.24 Supporting enums

namespace System.Runtime.InteropServices
{
   public enum CallingConvention
   {
      Winapi = 1,
      Cdecl = 2,
      Stdcall = 3,
      Thiscall = 4,
      Fastcall = 5
   }
   public enum CharSet
   {
      None,
      Auto,
      Ansi,
      Unicode
   }
   public enum ComInterfaceType
   {
      InterfaceIsDual = 0,
      InterfaceIsIUnknown = 1,
      InterfaceIsIDispatch = 2,
   }
   public enum LayoutKind
   {
       Sequential,
       Union,
       Explicit,
   }
   public enum TypeLibFuncFlags
   {
      FRestricted = 1,
      FSource = 2,
      FBindable = 4,
      FRequestEdit = 8,
      FDisplayBind = 16,
      FDefaultBind = 32,
      FHidden = 64,
      FUsesGetLastError = 128,
      FDefaultCollelem = 256,
      FUiDefault = 512,
      FNonBrowsable = 1024,
      FReplaceable = 2048,
      FImmediateBind = 4096
   }
   public enum TypeLibTypeFlags
   {
      FAppObject = 1,
      FCanCreate = 2,
      FLicensed = 4,
      FPreDeclId = 8,
      FHidden = 16,
      FControl = 32,
      FDual = 64,
      FNonExtensible = 128,
      FOleAutomation = 256,
      FRestricted = 512,
      FAggregatable = 1024,
      FReplaceable = 2048,
      FDispatchable = 4096,
      FReverseBind = 8192
   }
   public enum TypeLibVarFlags
   {
      FReadOnly = 1,
      FSource = 2,
      FBindable = 4,
      FRequestEdit = 8,
      FDisplayBind = 16,
      FDefaultBind = 32,
      FHidden = 64,
      FRestricted = 128,
      FDefaultCollelem = 256,
      FUiDefault = 512,
      FNonBrowsable = 1024,
      FReplaceable = 2048,
      FImmediateBind = 4096
   }
   public enum UnmanagedType
   {
      Bool         = 0x2,
      I1            = 0x3,
      U1            = 0x4,
      I2            = 0x5,
      U2            = 0x6,
      I4            = 0x7,
      U4            = 0x8,
      I8            = 0x9,
      U8            = 0xa,
      R4            = 0xb,
      R8            = 0xc,
      BStr         = 0x13,
      LPStr         = 0x14,
      LPWStr      = 0x15,
      LPTStr      = 0x16,
      ByValTStr   = 0x17,
      Struct      = 0x1b,
      Interface   = 0x1c,
      SafeArray   = 0x1d,
      ByValArray   = 0x1e,
      SysInt      = 0x1f,
      SysUInt      = 0x20,
      VBByRefStr   = 0x22,
      AnsiBStr      = 0x23,
      TBStr         = 0x24,
      VariantBool   = 0x25,
      FunctionPtr   = 0x26,
      LPVoid      = 0x27,
      AsAny         = 0x28,
      RPrecise      = 0x29,
      LPArray      = 0x2a,
      LPStruct      = 0x2b,
      CustomMarshaller = 0x2c,
   }
}


20. References

Unicode Consortium. The Unicode Standard, Version 3.0. Addison-Wesley, Reading, Massachusetts, 2000, ISBN 0-201-616335-5.

IEEEE. IEEE Standard for Binary Floating-Point Arithmetic. ANSI/IEEE Standard 754-1985. Available from http://www.ieee.org.

ISO/IEC. C++. ANSI/ISO/IEC 14882:1998.


21. C# Grammar Summary

This section summarizes the C# language grammar. For information on how to read the grammar, see §2.2.


21.1 Introduction Grammar

This section of the Language Reference does not have any grammar.


21.2 Lexical structure Grammar

2.2 Grammar notation

nonsense:
terminal1
terminal2
letter: one of
A B C a b c
letter: one of
A
B
C
a
b
c
whole:
first-part second-partopt last-part
whole:
first-part last-part
first-part second-part last-part

2.3 Pre-processing

pp-unit:
pp-groupopt
pp-group:
pp-group-part
pp-group pp-group-part
pp-group-part:
pp-tokensopt new-line
pp-declaration
pp-if-section
pp-control-line
pp-line-number
pp-tokens:
pp-token
pp-tokens pp-token
pp-token:
identifier
keyword
literal
operator-or-punctuator
new-line:
The carriage return character (U+000D)
The line feed character (U+000A)
The carriage return character followed by a line feed character
The line separator character (U+2028)
The paragraph separator character (U+2029)

2.3.1 Pre-processing declarations

pp-declaration:
#define pp-identifier
#undef pp-identifier

2.3.2 #if, #elif, #else, #endif

pp-if-section:
pp-if-group pp-elif-groupsopt pp-else-groupopt pp-endif-line
pp-if-group:
#if pp-expression new-line pp-groupopt
pp-elif-groups
pp-elif-group
pp-elif-groups pp-elif-group
pp-elif-group:
#elif pp-expression new-line groupopt
pp-else-group:
#else new-line groupopt
pp-endif-line
#endif new-line

2.3.3 Pre-processing control lines

pp-control-line:
#error pp-message
#warning pp-message
pp-message:
pp-tokensopt

2.3.4 #line

pp-line-number:
#line integer-literal
#line integer-literal string-literal
pp-integer-literal:
decimal-digit
decimal-digits decimal-digit
pp-string-literal:
" pp-string-literal-characters "
pp-string-literal-characters:
pp-string-literal-character
pp-string-literal-characters pp-string-literal-character
pp-string-literal-character:
Any character except " (U+0022), and white-space

2.3.5 Pre-processing identifiers

pp-identifier:
pp-available-identifier
pp-available-identifier:
A pp-identifier-or-keyword that is not true or false
pp-identifier-or-keyword:
identifier-start-character identifier-part-charactersopt

2.3.6 Pre-processing expressions

pp-expression:
pp-equality-expression
pp-primary-expression:
true
false
pp-identifier
( pp-expression )
pp-unary-expression:
pp-primary-expression
! pp-unary-expression
pp-equality-expression:
pp-equality-expression == pp-logical-and-expression
pp-equality-expression != pp-logical-and-expression
pp-logical-and-expression:
pp-unary-expression
pp-logical-and-expression && pp-unary-expression
pp-logical-or-expression:
pp-logical-and-expression
pp-logical-or-expression || pp-logical-and-expression

2.4.1 Input

input:
input-elementsopt
input-elements:
input-element
input-elements input-element
input-element:
comment
white-space
token

2.4.2 Input characters

input-character:
any Unicode character

2.4.3 Line terminators

line-terminator:
The carriage return character (U+000D)
The line feed character (U+000A)
The carriage return character followed by a line feed character
The line separator character (U+2028)
The paragraph separator character (U+2029)

2.4.4 Comments

comment:
regular-comment
one-line-comment
regular-comment:
/ * rest-of-regular-comment
rest-of-regular-comment:
* rest-of-regular-comment-star
not-star rest-of-regular-comment
rest-of-regular-comment-star:
/
* rest-of-regular-comment-star
not-star-or-slash rest-of-regular-comment
not-star:
Any input-character except *
not-star-or-slash:
Any input-character except * and /
one-line-comment:
/ / one-line-comment-text line-terminator
one-line-comment-text:
input-character
one-line-comment-text input-character

2.4.5 White space

white-space:
new-line
The tab character (U+0009)
The vertical tab character (U+000B)
The form feed character (U+000C)
The "control-Z" or "substitute" character (U+001A)
All characters with Unicode class "Zs"

2.4.6 Tokens

There are several kinds of tokens: identifiers, keywords, literals, operators, and punctuators. White space is ignored, though it may act as a separator for tokens.token:
identifier
keyword
literal
operator-or-punctuator

2.5.1 Identifiers

identifier:
available-identifier
@ identifier-or-keyword
available-identifier:
An identifier-or-keyword that is not a keyword
identifier-or-keyword:
identifier-start-character identifier-part-charactersopt
identifier-start-character:
letter-character
connecting-character
identifier-part-characters:
identifier-part-character
identifier-part-characters identifier-part-character
identifier-part-character:
letter-character
decimal-digit-character
connecting-character
combining-character
formatting-character
letter-character:
A Unicode character of classes Lu, Ll, Lt, Lm, Lo, or Nl
A unicode-character-escape-sequence representing a character of classes Lu, Ll, Lt, Lm, Lo, or Nl
combining-character:
A Unicode character of classes Mn or Mc
A unicode-character-escape-sequence representing a character of classes Mn or Mc
decimal-digit-character:
A Unicode character of the class Nd
A unicode-character-escape-sequence representing a character of the class Nd
connecting-character:
A Unicode character of the class Pc
A unicode-character-escape-sequence representing a character of the class Pc
formatting-character:
A Unicode character of the class Cf
A unicode-character-escape-sequence representing a character of the class Cf

2.5.2 Keywords

keyword: one of
abstract as base bool break
byte case catch char checked
class const continue decimal default
delegate do double else enum
event explicit extern false finally
fixed float for foreach goto
if implicit in int interface
internal is lock long namespace
new null object operator out
override params private protected public
readonly ref return sbyte sealed
short sizeof stackalloc static string
struct switch this throw true
try typeof uint ulong unchecked
unsafe ushort using virtual void
while

2.5.3 Literals

literal:
boolean-literal
integer-literal
real-literal
character-literal
string-literal
null-literal

2.5.3.2 Integer literals

integer-literal:
decimal-integer-literal
hexadecimal-integer-literal
decimal-integer-literal:
decimal-digits integer-type-suffixopt
decimal-digits:
decimal-digit
decimal-digits decimal-digit
decimal-digit: one of
0 1 2 3 4 5 6 7 8 9
integer-type-suffix: one of
U u L l UL Ul uL ul LU Lu lU lu
hexadecimal-integer-literal:
0x hex-digits integer-type-suffixopt
hex-digits:
hex-digit
hex-digits hex-digit
hex-digit: one of
0 1 2 3 4 5 6 7 8 9 A B C D E F a b c d e f

2.5.3.3 Real literals

real-literal:
decimal-digits . decimal-digits exponent-partopt real-type-suffixopt
. decimal-digits exponent-partopt real-type-suffixopt
decimal-digits exponent-part real-type-suffixopt
decimal-digits real-type-suffix
exponent-part:
e signopt decimal-digits
E signopt decimal-digits
sign: one of
+ -
real-type-suffix: one of
F f D d M m

2.5.3.4 Character literals

character-literal:
' character '
character:
single-character
simple-escape-sequence
hexadecimal-escape-sequence
unicode-character-escape-sequence
single-character:
Any character except ' (U+0027), \ (U+005C), and new-line
simple-escape-sequence: one of
\' \" \\ \0 \a \b \f \n \r \t \v
hexadecimal-escape-sequence:
\x hex-digit hex-digitopt hex-digitopt hex-digitopt

2.5.3.5 String literals

string-literal:
regular-string-literal
verbatim-string-literal
regular-string-literal:
" regular-string-literal-charactersopt "
regular-string-literal-characters:
regular-string-literal-character
regular-string-literal-characters regular-string-literal-character
regular-string-literal-character:
single-regular-string-literal-character
simple-escape-sequence
hexadecimal-escape-sequence
unicode-character-escape-sequence
single-regular-string-literal-character:
Any character except " (U+0022), \ (U+005C), and new-line
verbatim-string-literal:
@" verbatim -string-literal-charactersopt "
verbatim-string-literal-characters:
verbatim-string-literal-character
verbatim-string-literal-characters verbatim-string-literal-character
verbatim-string-literal-character:
single-verbatim-string-literal-character
quote-escape-sequence
single-verbatim-string-literal-character:
any character except "
quote-escape-sequence:
""

2.5.3.6 The null literal

null-literal:
null

2.5.4 Operators and punctuators

operator-or-punctuator: one of
{ } [ ] ( ) . , : ;
+ - * / % & | ^ ! ~
= < > ? ++ -- && || << >>
== != <= >= += -= *= /= %= &=
|= ^= <<= >>= ->

2.5.5 Unicode character escape sequences

unicode-character-escape-sequence:
\u hex-digit hex-digit hex-digit hex-digit


21.3 Basic concepts Grammar

3.6 Namespace and type names

namespace-name:
namespace-or-type-name
type-name:
namespace-or-type-name
namespace-or-type-name:
identifier
namespace-or-type-name . identifier


21.4 Types Grammar

4.1 Value types

value-type:
struct-type
enum-type
struct-type:
type-name
simple-type
simple-type:
numeric-type
bool
numeric-type:
integral-type
floating-point-type
decimal
integral-type:
sbyte
byte
short
ushort
int
uint
long
ulong
char
floating-point-type:
float
double
enum-type:
type-name

4.2 Reference types

reference-type:
class-type
interface-type
array-type
delegate-type
class-type:
type-name
object
string
interface-type:
type-name
array-type:
non-array-type rank-specifiers
non-array-type:
type
rank-specifiers:
rank-specifier
rank-specifiers rank-specifier
rank-specifier:
[ dim-separatorsopt ]
dim-separators:
,
dim-separators ,
delegate-type:
type-name


21.5 Variables Grammar

5.4 Variable references

variable-reference:
expression


21.6 Conversions Grammar

This section of the Language Reference does not have any grammar.


21.7 Expressions Grammar

7.4.1 Argument lists

argument-list:
argument
argument-list , argument
argument:
expression
ref variable-reference
out variable-reference

7.5 Primary expressions

primary-expression:
literal
simple-name
parenthesized-expression
member-access
invocation-expression
element-access
this-access
base-access
post-increment-expression
post-decrement-expression
new-expression
typeof-expression
sizeof-expression
checked-expression
unchecked-expression

7.5.2 Simple names

simple-name:
identifier

7.5.3 Parenthesized expressions

parenthesized-expression:
( expression )

7.5.4 Member access

member-access:
primary-expression . identifier
predefined-type . identifier
predefined-type: one of
bool byte char decimal double float int long
object sbyte short string uint ulong ushort

7.5.5 Invocation expressions

invocation-expression:
primary-expression ( argument-listopt )

7.5.6 Element access

element-access:
primary-expression [ expression-list ]
expression-list:
expression
expression-list , expression

7.5.7 This access

this-access:
this

7.5.8 Base access

base-access:
base . identifier
base [ expression-list ]

7.5.9 Postfix increment and decrement operators

post-increment-expression:
primary-expression ++
post-decrement-expression:
primary-expression --

7.5.10 new operator

new-expression:
object-creation-expression
array-creation-expression
delegate-creation-expression

7.5.10.1 Object creation expressions

object-creation-expression:
new type ( argument-listopt )

7.5.10.2 Array creation expressions

array-creation-expression:
new non-array-type [ expression-list ] rank-specifiersopt array-initializeropt
new array-type array-initializer

7.5.10.3 Delegate creation expressions

delegate-creation-expression:
new delegate-type ( expression )

7.5.11 typeof operator

typeof-expression:
typeof ( type )

7.5.12 checked and unchecked operators

checked-expression:
checked ( expression )
unchecked-expression:
unchecked ( expression )

7.6 Unary expressions

unary-expression:
primary-expression
+ unary-expression
- unary-expression
! unary-expression
~ unary-expression
* unary-expression
& unary-expression
pre-increment-expression
pre-decrement-expression
cast-expression

7.6.7 Prefix increment and decrement operators

pre-increment-expression:
++ unary-expression
pre-decrement-expression:
-- unary-expression

7.6.8 Cast expressions

cast-expression:
( type ) unary-expression

7.7 Arithmetic operators

multiplicative-expression:
unary-expression
multiplicative-expression * unary-expression
multiplicative-expression / unary-expression
multiplicative-expression % unary-expression
additive-expression:
multiplicative-expression
additive-expression + multiplicative-expression
additive-expression – multiplicative-expression

7.8 Shift operators

shift-expression:
additive-expression
shift-expression << additive-expression
shift-expression >> additive-expression

7.9 Relational operators

relational-expression:
shift-expression
relational-expression < shift-expression
relational-expression > shift-expression
relational-expression <= shift-expression
relational-expression >= shift-expression
relational-expression is reference-type
equality-expression:
relational-expression
equality-expression == relational-expression
equality-expression != relational-expression

7.10 Logical operators

and-expression:
equality-expression
and-expression & equality-expression
exclusive-or-expression:
and-expression
exclusive-or-expression ^ and-expression
inclusive-or-expression:
exclusive-or-expression
inclusive-or-expression | exclusive-or-expression

7.11 Conditional logical operators

conditional-and-expression:
inclusive-or-expression
conditional-and-expression && inclusive-or-expression
conditional-or-expression:
conditional-and-expression
conditional-or-expression || conditional-and-expression

7.12 Conditional operator

conditional-expression:
conditional-or-expression
conditional-or-expression ? expression : expression

7.13 Assignment operators

assignment:
unary-expression assignment-operator expression
assignment-operator: one of
= += -= *= /= %= &= |= ^= <<= >>=

7.14 Expression

expression:
conditional-expression
assignment

7.15 Constant expressions

constant-expression:
expression

7.16 Boolean expressions

boolean-expression:
expression


21.8 Statements Grammar

8.2 Blocks

block:
{ statement-listopt }

8.2.1 Statement lists

statement-list:
statement
statement-list statement

8.3 The empty statement

empty-statement:
;

8.4 Labeled statements

labeled-statement:
identifier : statement

8.5 Declaration statements

declaration-statement:
local-variable-declaration ;
local-constant-declaration ;

8.5.1 Local variable declarations

local-variable-declaration:
type variable-declarators
variable-declarators:
variable-declarator
variable-declarators , variable-declarator
variable-declarator:
identifier
identifier = variable-initializer
variable-initializer:
expression
array-initializer

8.5.2 Local constant declarations

local-constant-declaration:
const type constant-declarators
constant-declarators:
constant-declarator
constant-declarators , constant-declarator
constant-declarator:
identifier = constant-expression

8.6 Expression statements

expression-statement:
statement-expression ;
statement-expression:
invocation-expression
object-creation-expression
assignment
post-increment-expression
post-decrement-expression
pre-increment-expression
pre-decrement-expression

8.7 Selection statements

selection-statement:
if-statement
switch-statement

8.7.1 The if statement

if-statement:
if ( boolean-expression ) embedded-statement
if ( boolean-expression ) embedded-statement else embedded-statement
boolean-expression:
expression

8.7.2 The switch statement

switch-statement:
switch ( expression ) switch-block
switch-block:
{ switch-sectionsopt }
switch-sections:
switch-section
switch-sections switch-section
switch-section:
switch-labels statement-list
switch-labels:
switch-label
switch-labels switch-label
switch-label:
case constant-expression :
default :

8.8 Iteration statements

iteration-statement:
while-statement
do-statement
for-statement
foreach-statement

8.8.1 The while statement

while-statement:
while ( boolean-expression ) embedded-statement

8.8.2 The do statement

do-statement:
do embedded-statement while ( boolean-expression ) ;

8.8.3 The for statement

for-statement:
for ( for-initializeropt ; for-conditionopt ; for-iteratoropt ) embedded-statement
for-initializer:
local-variable-declaration
statement-expression-list
for-condition:
boolean-expression
for-iterator:
statement-expression-list
statement-expression-list:
statement-expression
statement-expression-list , statement-expression

8.8.4 The foreach statement

foreach-statement:
foreach ( type identifier in expression ) embedded-statement

8.9 Jump statements

jump-statement:
break-statement
continue-statement
goto-statement
return-statement
throw-statement

8.9.1 The break statement

break-statement:
break ;

8.9.2 The continue statement

continue-statement:
continue ;

8.9.3 The goto statement

goto-statement:
goto identifier ;
goto case constant-expression ;
goto default ;

8.9.4 The return statement

return-statement:
return expressionopt ;

8.9.5 The throw statement

throw-statement:
throw expressionopt ;

8.10 The try statement

try-statement:
try block catch-clauses
try block finally-clause
try block catch-clauses finally-clause
catch-clauses:
specific-catch-clauses general-catch-clauseopt
specific-catch-clausesopt general-catch-clause
specific-catch-clauses:
specific-catch-clause
specific-catch-clauses specific-catch-clause
specific-catch-clause:
catch ( class-type identifieropt ) block
general-catch-clause:
catch block
finally-clause:
finally block

8.11 The checked and unchecked statements

checked-statement:
checked block
unchecked-statement:
unchecked block

8.12 The lock statement

lock-statement:
lock ( expression ) embedded-statement

8.13 The using statement

using-statement:
using ( resource-acquisition ) embedded-statement
resource-acquisition:
local-variable-declaration
expression


21.9 Namespaces Grammar

9.1 Compilation units

compilation-unit:
using-directivesopt namespace-member-declarationsopt

9.2 Namespace declarations

namespace-declaration:
namespace qualified-identifier namespace-body ;opt
qualified-identifier:
identifier
qualified-identifier . identifier
namespace-body:
{ using-directivesopt namespace-member-declarationsopt }

9.3 Using directives

using-directives:
using-directive
using-directives using-directive
using-directive:
using-alias-directive
using-namespace-directive

9.3.1 Using alias directives

using-alias-directive:
using identifier = namespace-or-type-name ;

9.3.2 Using namespace directives

using-namespace-directive:
using namespace-name ;

9.4 Namespace members

namespace-member-declarations:
namespace-member-declaration
namespace-member-declarations namespace-member-declaration
namespace-member-declaration:
namespace-declaration
type-declaration

9.5 Type declarations

type-declaration:
class-declaration
struct-declaration
interface-declaration
enum-declaration
delegate-declaration


21.10 Classes Grammar

10.1 Class declarations

class-declaration:
attributesopt class-modifiersopt class identifier class-baseopt class-body ;opt

10.1.1 Class modifiers

class-modifiers:
class-modifier
class-modifiers class-modifier
class-modifier:
new
public
protected
internal
private
abstract
sealed

10.1.2 Class base specification

class-base:
: class-type
: interface-type-list
: class-type , interface-type-list
interface-type-list:
interface-type
interface-type-list , interface-type

10.1.3 Class body

class-body:
{ class-member-declarationsopt }

10.2 Class members

class-member-declarations:
class-member-declaration
class-member-declarations class-member-declaration
class-member-declaration:
constant-declaration
field-declaration
method-declaration
property-declaration
event-declaration
indexer-declaration
operator-declaration
constructor-declaration
destructor-declaration
static-constructor-declaration
type-declaration

10.3 Constants

constant-declaration:
attributesopt constant-modifiersopt const type constant-declarators ;
constant-modifiers:
constant-modifier
constant-modifiers constant-modifier
constant-modifier:
new
public
protected
internal
private
constant-declarators:
constant-declarator
constant-declarators , constant-declarator
constant-declarator:
identifier = constant-expression

10.4 Fields

field-declaration:
attributesopt field-modifiersopt type variable-declarators ;
field-modifiers:
field-modifier
field-modifiers field-modifier
field-modifier:
new
public
protected
internal
private
static
readonly
variable-declarators:
variable-declarator
variable-declarators , variable-declarator
variable-declarator:
identifier
identifier = variable-initializer
variable-initializer:
expression
array-initializer

10.5 Methods

method-declaration:
method-header method-body
method-header:
attributesopt method-modifiersopt return-type member-name ( formal-parameter-listopt )
method-modifiers:
method-modifier
method-modifiers method-modifier
method-modifier:
new
public
protected
internal
private
static
virtual
override
abstract
extern
return-type:
type
void
member-name:
identifier
interface-type . identifier
method-body:
block
;

10.5.1 Method parameters

formal-parameter-list:
fixed-parameters
fixed-parameters , parameter-array
parameter-array
fixed-parameters:
fixed-parameter
fixed-parameters , fixed-parameter
fixed-parameter:
attributesopt parameter-modifieropt type identifier
parameter-modifier:
ref
out
parameter-array:
attributesopt params array-type identifier

10.6 Properties

property-declaration:
attributesopt property-modifiersopt type member-name { accessor-declarations }
property-modifiers:
property-modifier
property-modifiers property-modifier
property-modifier:
new
public
protected
internal
private
static
virtual
override
abstract
member-name:
identifier
interface-type . identifier

10.6.5 Accessors

accessor-declarations:
get-accessor-declaration set-accessor-declarationopt
set-accessor-declaration get-accessor-declarationopt
get-accessor-declaration:
attributesopt get accessor-body
set-accessor-declaration:
attributesopt set accessor-body
accessor-body:
block
;

10.7 Events

event-declaration:
event-field-declaration
event-property-declaration
event-field-declaration:
attributesopt event-modifiersopt event type variable-declarators ;
event-property-declaration:
attributesopt event-modifiersopt event type member-name { accessor-declarations }
event-modifiers:
event-modifier
event-modifiers event-modifier
event-modifier:
new
public
protected
internal
private
static

10.8 Indexers

indexer-declaration:
attributesopt indexer-modifiersopt indexer-declarator { accessor-declarations }
indexer-modifiers:
indexer-modifier
indexer-modifiers indexer-modifier
indexer-modifier:
new
public
protected
internal
private
virtual
override
abstract
indexer-declarator:
type this [ formal-parameter-list ]
type interface-type . this [ formal-parameter-list ]

10.9 Operators

operator-declaration:
attributesopt operator-modifiers operator-declarator block
operator-modifiers:
public static
static public
operator-declarator:
unary-operator-declarator
binary-operator-declarator
conversion-operator-declarator
unary-operator-declarator:
type operator overloadable-unary-operator ( type identifier )
overloadable-unary-operator: one of
+ - ! ~ ++ -- true false
binary-operator-declarator:
type operator overloadable-binary-operator ( type identifier , type identifier )
overloadable-binary-operator: one of
+ - * / % & | ^ << >> == != > < >= <=
conversion-operator-declarator:
implicit operator type ( type identifier )
explicit operator type ( type identifier )

10.10 Instance constructors

constructor-declaration:
attributesopt constructor-modifiersopt constructor-declarator block
constructor-modifiers:
constructor-modifier
constructor-modifiers constructor-modifier
constructor-modifier:
public
protected
internal
private
constructor-declarator:
identifier ( formal-parameter-listopt ) constructor-initializeropt
constructor-initializer:
: base ( argument-listopt )
: this ( argument-listopt )

10.11 Destructors

destructor-declaration:
attributesopt ~ identifier ( ) block

10.12 Static constructors

static-constructor-declaration:
attributesopt static identifier ( ) block


21.11 Structs Grammar

11.1 Struct declarations

struct-declaration:
attributesopt struct-modifiersopt struct identifier struct-interfacesopt struct-body ;opt

11.1.1 Struct modifiers

struct-modifiers:
struct-modifier
struct-modifiers struct-modifier
struct-modifier:
new
public
protected
internal
private

11.1.2 Interfaces

struct-interfaces:
: interface-type-list

11.1.3 Struct body

struct-body:
{ struct-member-declarationsopt }

11.2 Struct members

struct-member-declarations:
struct-member-declaration
struct-member-declarations struct-member-declaration
struct-member-declaration:
class-member-declaration


21.12 Arrays Grammar

12.1 Array types

array-type:
non-array-type rank-specifiers
non-array-type:
type
rank-specifiers:
rank-specifier
rank-specifiers rank-specifier
rank-specifier:
[ dim-separatorsopt ]
dim-separators:
,
dim-separators ,

12.6 Array initializers

array-initializer:
{ variable-initializer-listopt }
{ variable-initializer-list , }
variable-initializer-list:
variable-initializer
variable-initializer-list , variable-initializer
variable-initializer:
expression
array-initializer


21.13 Interfaces Grammar

13.1 Interface declarations

interface-declaration:
attributesopt interface-modifiersopt interface identifier interface-baseopt interface-body ;opt

13.1.1 Interface modifiers

interface-modifiers:
interface-modifier
interface-modifiers interface-modifier
interface-modifier:
new
public
protected
internal
private

13.1.2 Base interfaces

interface-base:
: interface-type-list

13.1.3 Interface body

interface-body:
{ interface-member-declarationsopt }

13.2 Interface members

interface-member-declarations:
interface-member-declaration
interface-member-declarations interface-member-declaration
interface-member-declaration:
interface-method-declaration
interface-property-declaration
interface-event-declaration
interface-indexer-declaration

13.2.1 Interface methods

interface-method-declaration:
attributesopt newopt return-type identifier ( formal-parameter-listopt ) ;

13.2.2 Interface properties

interface-property-declaration:
attributesopt newopt type identifier { interface-accessors }
interface-accessors:
attributesopt get ;
attributesopt set ;
attributesopt get ; attributesopt set ;
attributesopt set ; attributesopt get ;

13.2.3 Interface events

interface-event-declaration:
attributesopt newopt event type identifier ;

13.2.4 Interface indexers

interface-indexer-declaration:
attributesopt newopt type this [ formal-parameter-list ] { interface-accessors }


21.14 Enums Grammar

14.1 Enum declarations

enum-declaration:
attributesopt enum-modifiersopt enum identifier enum-baseopt enum-body ;opt
enum-modifiers:
enum-modifier
enum-modifiers enum-modifier
enum-modifier:
new
public
protected
internal
private
enum-base:
: integral-type
enum-body:
{ enum-member-declarationsopt }
{ enum-member-declarations , }

14.2 Enum members

enum-member-declarations:
enum-member-declaration
enum-member-declarations , enum-member-declaration
enum-member-declaration:
attributesopt identifier
attributesopt identifier = constant-expression


21.15 Delegates Grammar

15.1 Delegate declarations

delegate-declaration:
attributesopt delegate-modifiersopt delegate result-type identifier ( formal-parameter-listopt ) ;
delegate-modifiers:
delegate-modifier
delegate-modifiers delegate-modifier
delegate-modifier:
new
public
protected
internal
private


21.16 Exceptions Grammar

This section of the Language Reference does not have any grammar.


21.17 Attributes Grammar

17.2 Attribute specification

attributes:
attribute-sections
attribute-sections:
attribute-section
attribute-sections attribute-section
attribute-section:
[ attribute-list ]
[ attribute-list ,]
attribute-list:
attribute
attribute-list , attribute
attribute:
attribute-name attribute-argumentsopt
attribute-name:
reserved-attribute-name
type-name
attribute-arguments:
( positional-argument-list )
( positional-argument-list , named-argument-list )
( named-argument-list )
positional-argument-list:
positional-argument
positional-argument-list , positional-argument
positional-argument:
attribute-argument-expression
named-argument-list:
named-argument
named-argument-list , named-argument
named-argument:
identifier = attribute-argument-expression
attribute-argument-expression:
expression


21.18 Unsafe code Grammar

18.2 Pointer types

type:
value-type
reference-type
pointer-type
pointer-type:
unmanaged-type *
void *
unmanaged-type:
type

18.4 Pointer operations

primary-expression:
...
pointer-member-access
pointer-element-access
sizeof-expression
stackalloc-expression
unary-expression:
...
pointer-indirection-expression
addressof-expression

18.4.1 Pointer member access

pointer-member-access:
primary-expression -> identifier

18.4.2 Pointer element access

pointer-element-access:
primary-expression [ expression ]

18.4.3 The sizeof operator

sizeof-expression:
sizeof ( type )

18.4.4 The stackalloc operator

stackalloc-expression:
stackalloc type [ expression ]

18.4.5 Pointer indirection

pointer-indirection-expression:
* unary-expression

18.4.6 The address-of operator

addressof-expression:
& unary-expression

18.5 The fixed statement

fixed-statement:
fixed ( pointer-type fixed-pointer-declarators ) embedded-statement
fixed-pointer-declarators:
fixed-pointer-declarator
fixed-pointer-declarators , fixed-pointer-declarator
fixed-pointer-declarator:
identifier = & variable-reference
identifier = expression


21.19 Interoperability Grammar

This section of the Language Reference does not have any grammar.


21.20 References Grammar

This section of the Language Reference does not have any grammar.


C# Programmer's Reference

C# Programmer's Reference

The C# Programmer's Reference complements the C# Language Reference by providing tutorials, reference material, and topics on how to program in C#.

For information about | See
Learning the C# language | C# Tutorials
Keywords | C# Keywords
Operators | C# Operators
Attributes | C# Attributes
Conditional compilation | C# Preprocessor Directives
Types | Types Reference Tables
Arrays, Main method, properties, indexers, and other language features | C# Language Features
Documenting your code using XML | XML Documentation

See Also

C# Language Reference | Programming with C#


C# Tutorials

The C# tutorials provide an overview of the basics of the language and identify important language features. Each tutorial includes one or more sample programs. The tutorials discuss important lines in the samples plus provide additional background information. They also link to the sample abstract topics, where you can download and run the sample programs.

Tutorial | Description | Level
Hello World | Shows a Hello World application. | Simple
Command Line Parameters | Shows simple command-line processing; also shows array indexing. | Simple
Arrays | Shows how to use arrays. | Simple
Properties | Shows how properties are declared and used; also demonstrates abstract properties. | Simple
Libraries | Shows how to use compiler options to create a DLL library from multiple source files; also, how to use them in other programs. | Simple
Versioning | Shows the use of override and new to support versioning. | Simple
Collection Classes | Shows how to make collection classes that can be used with foreach. | Intermediate
Structs | Shows how to use structs in C#. | Intermediate
Indexers | Shows how to use array notation to access an object. | Intermediate
User-Defined Conversions | Shows how to define conversions to and from user-defined types. | Intermediate
Operator Overloading | Shows how user-defined classes can overload operators. | Intermediate
Delegates | Shows how delegates are declared, mapped, and combined. | Intermediate
Events | Shows how to use events in C#. | Intermediate
Explicit Interface Implementation | Demonstrates how to explicitly implement interface members. | Intermediate
Conditional Methods | Demonstrates conditional methods, which provide a powerful mechanism by which calls to methods can be included or omitted depending on whether a symbol is defined. | Intermediate
XML Documentation | Shows how to document code by using XML. | Intermediate
Platform Invoke | Shows how to call platform invokes (exported DLL functions) from C#. | Advanced
COM InterOp | Shows how to use C# to perform unmanaged COM InterOp. | Advanced
Attributes | Shows how to create custom attribute classes, use them in code, and query them through reflection. | Advanced
Security | Discusses .NET security and shows two ways to modify security permissions in C#: permission classes and permission attributes. | Advanced
Threading |  | Advanced
Unsafe Code | Shows how to use pointers. | Advanced
ADO | Shows how to use ADO in C#. | Advanced

See Also

C# Tutorial Samples | C# Programmer's Reference


Hello World Tutorial

This tutorial shows several versions of a Hello World program in C#.

Sample Files

See Hello World Sample to download and build and build the sample files discussed in this tutorial.

Further Reading

  • General Structure of a C# Program
  • Main
  • §1.1 Hello, world
  • §3.1 Declarations

Tutorial

The following examples show different ways of writing the C# Hello World program.

Example 1

000: // HelloWorld\Hello1.cs
001: public class Hello1
002: {
003:     public static void Main()
004:     {
005:         System.Console.WriteLine("Hello, World!");
006:     }
007: }

Output

Hello, World!

Code Discussion

  • Every Main method must be contained inside a class (Hello1 in this case).
  • The System.Console class contains a WriteLine method that can be used to display a string to the console.

Example 2

To avoid fully qualifying classes throughout a program, you can use the using directive as shown below:

000: // HelloWorld\Hello2.cs
001: using System;
002:
003: public class Hello2
004: {
005:     public static void Main()
006:     {
007:         Console.WriteLine("Hello, World!");
008:     }
009: }

Output

Hello, World!

Example 3

If you need access to the command line parameters passed in to your application, simply change the signature of the Main method to include them as shown below. This example counts and displays the command line arguments.

000: // HelloWorld\Hello3.cs
001: using System;
002:
003: public class Hello3
004: {
005:   public static void Main(string[] args)
006:   {
007:     Console.WriteLine("Hello, World!");
008:     Console.WriteLine("You entered the following {0} command line arguments:", args.Length );
009:     for (int i=0; i < args.Length; i++)
010:     {
011:        Console.WriteLine("{0}", args[i]);
012:     }
013:   }
014: }

Sample Run

Run the program using some arguments like this:

Hello3 A B C D

The output will be:

Hello, World!
You entered the following 4 command line arguments:
A
B
C
D

Example 4

To return a return code, change the signature of the Main method as shown below:

000: // HelloWorld\Hello4.cs
001: using System;
002:
003: public class Hello4
004: {
005:   public static int Main(string[] args)
006:   {
007:     Console.WriteLine("Hello, World!");
008:     return 0;
009:   }
010: }

Output

Hello, World!

See Also

C# Tutorials


Command Line Parameters Tutorial

This tutorial shows how the command line can be accessed and two ways of accessing the array of command line parameters.

Sample Files

See Command Line Parameters Sample to download and build the sample files discussed in this tutorial.

Further Reading

  • The C# Version of Hello World!
  • foreach, in
  • String.Length

Tutorial

The following examples show two different approaches to using the command line arguments passed to an application.

Example 1

This example demonstrates how to print out the command line arguments.

000: // CommandLine\cmdline1.cs
001: using System;
002:
003: public class CommandLine
004: {
005:     public static void Main(string[] args)
006:     {
007:         Console.WriteLine("Number of command line parameters = {0}", args.Length);
008:         for(int i = 0; i < args.Length; i++)
009:         {
010:             Console.WriteLine("Arg[{0}] = [{1}]", i, args[i]);
011:         }
012:     }
013: }

Sample Run

Run the program using some arguments like this:

cmdline1 A B C

The output will be:

Number of command line parameters = 3
Arg[0] = [A]
Arg[1] = [B]
Arg[2] = [C]

Code Discussion

In line 007, the Length property is used to obtain the length of the array. Notice that Length is a read-only property.

Example 2

Another approach to iterating over the array is to use the foreach statement as shown in this example. The foreach statement can be used to iterate over an array or over a .NET Frameworks collection class. It provides a simple way to iterate over collections.

000: // CommandLine\cmdline2.cs
001: using System;
002:
003: public class CommandLine2
004: {
005:     public static void Main(string[] args)
006:     {
007:         Console.WriteLine("Number of command line parameters = {0}", args.Length);
008:         foreach(string s in args)
009:         {
010:             Console.WriteLine(s);
011:         }
012:     }
013: }

Sample Run

>cmdline2 John Paul Mary
Number of command line parameters = 3
John
Paul
Mary

See Also

C# Tutorials | Collection Classes Tutorial


Arrays Tutorial

This tutorial describes arrays and shows how they work in C#.

Sample Files

See Arrays Sample to download and build the sample files discussed in this tutorial.

Further Reading

  • Arrays
  • §1.5 Array types
  • foreach, in
  • Collection Classes Tutorial

Tutorial

This tutorial is divided into the following sections:

  • Arrays in General
  • Declaring Arrays
  • Initializing Arrays
  • Accessing Array Members
  • Arrays Are Objects
  • Using foreach with Arrays

Arrays in General

C# arrays are zero indexed; that is, the array indexes start at zero. Arrays in C# work similarly to how arrays work in most other popular languages There are, however, a few differences that you should be aware of.

When declaring an array, the square brackets ([]) must come after the type, not the identifier. Placing the brackets after the identifier is not legal syntax in C#.

int[] table; // not int table[];

Another detail is that the size of the array is not part of its type as it is in the C language. This allows you to declare an array and assign any array of int objects to it, regardless of the array's length.

int[] numbers; // declare numbers as an int array of any size
numbers = new int[10];  // numbers is a 10-element array
numbers = new int[20];  // now it's a 20-element array

Declaring Arrays

C# supports single-dimensional arrays, multidimensional arrays (rectangular arrays), and array-of-arrays (jagged arrays). The syntax for declaring arrays is as follows:

Single-dimensional arrays:

int[] numbers;

Multidimensional arrays:

string[,] names;

Array-of-arrays (jagged):

byte[][] scores;

Declaring them (as shown above) does not actually create the arrays. In C#, arrays are objects (discussed later in this tutorial) and must be instantiated. The syntax to create an array is:

Single-dimensional arrays:

int[] numbers = new int[5];

Multidimensional arrays:

string[,] names = new string[5,4];

Array-of-arrays (jagged):

byte[][] scores = new byte[5][];
for (int x = 0; x < scores.Length; x++)
{
   scores[x] = new byte[4];
}

You can also have larger arrays. For example, you can have a three-dimensional rectangular array:

int[,,] buttons = new int[4,5,3];

You can even mix rectangular and jagged arrays. For example, the following code declares a single-dimensional array of three-dimensional arrays of two-dimensional arrays of type int:

int[][,,][,] numbers;

Example

The following is a complete C# program that declares and instantiates arrays as discussed above.

000: // Arrays\arrays.cs
001: using System;
002: class DeclareArraysSample
003: {
004:     public static void Main()
005:     {
006:         // Single-dimensional array
007:         int[] numbers = new int[5];
008:
009:         // Multidimensional array
010:         string[,] names = new string[5,4];
011:
012:         // Array-of-arrays (jagged array)
013:         byte[][] scores = new byte[5][];
014:
015:         // Create the jagged array
016:         for (int i = 0; i < scores.Length; i++)
017:         {
018:             scores[i] = new byte[i+3];
019:         }
020:
021:         // Print length of each row
022:         for (int i = 0; i < scores.Length; i++)
023:         {
024:             Console.WriteLine("Length of row {0} is {1}", i, scores[i].Length);
025:         }
026:     }
027: }

Output

Length of row 0 is 3
Length of row 1 is 4
Length of row 2 is 5
Length of row 3 is 6
Length of row 4 is 7

Initializing Arrays

C# provides simple and straightforward ways to initialize arrays at declaration-time, by enclosing the initial values in curly braces ( {} ). It is important to note that array members are automatically initialized to the default initial value for the array type if the array is not initialized at the time it is declared.

The following examples show different ways to initialize different kinds of arrays.

Single-Dimensional Array

int[] numbers = new int[5] {1, 2, 3, 4, 5};
string[] names = new string[3] {"Matt", "Joanne", "Robert"};

You can omit the size of the array, like this:

int[] numbers = new int[] {1, 2, 3, 4, 5};
string[] names = new string[] {"Matt", "Joanne", "Robert"};

You can also omit the new statement if an initializer is provided, like this:

int[] numbers = {1, 2, 3, 4, 5};
string[] names = {"Matt", "Joanne", "Robert"};

Multidimensional Array

int[,] numbers = new int[3, 2] { {1, 2}, {3, 4}, {5, 6} };
string[,] siblings = new string[2, 2] { {"Mike","Amy"}, {"Mary","Albert"} };

You can omit the size of the array, like this:

int[,] numbers = new int[,] { {1, 2}, {3, 4}, {5, 6} };
string[,] siblings = new string[,] { {"Mike","Amy"}, {"Mary","Ray"} };

You can also omit the new statement if an initializer is provided, like this:

int[,] numbers = { {1, 2}, {3, 4}, {5, 6} };
string[,] siblings = { {"Mike", "Amy"}, {"Mary", "Albert"} };

Jagged Array (Array-of-Arrays)

You can also initialize jagged arrays using this style. For example:

int[][] numbers = new int[2][] { new int[] {2,3,4}, new int[] {5,6,7,8,9} };

You can also omit the size of the first array, like this:

int[][] numbers = new int[][] { new int[] {2,3,4}, new int[] {5,6,7,8,9} };

Accessing Array Members

Accessing array members is straightforward and similar to how you access array members in C/C++. For example, the following code creates an array called numbers and then assigns a 5 to the fifth element of the array:

int[] numbers = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
numbers[4] = 5;

The following code declares a multidimensional array and assigns 5 to the member located at [1, 1]:

int[,] numbers = { {1, 2}, {3, 4}, {5, 6}, {7, 8}, {9, 10} };
numbers[1, 1] = 5;

With jagged arrays, accessing a member is done as follows:

int[][] numbers = new int[][]
{
   new int[] {1, 2},
   new int[] {3, 4}
};
numbers[1][1] = 5;

Arrays Are Objects

In C#, arrays are actually objects. System.Array is the abstract base type of all array types. You can use the properties, and other class members, that System.Array has. An example of this would be using the Length property to get the length of an array. The following code assigns the length of the numbers array, which is 5, to a variable called LengthOfNumbers:

int[] numbers = {1, 2, 3, 4, 5};
int LengthOfNumbers = numbers.Length;

The System.Array class provides many other useful methods/properties, such as methods for sorting, searching, and copying arrays.

Using foreach on Arrays

C# also provides the foreach statement. This statement provides a simple, clean way to iterate through the elements of an array. For example, the following code creates an array called numbers and iterates through it with the foreach statement:

int[] numbers = {4, 5, 6, 1, 2, 3, -2, -1, 0};
foreach (int i in numbers)
{
   System.Console.WriteLine(i);
}

See Also

C# Tutorials


Properties Tutorial

This tutorial shows how properties are an integral part of the C# programming language. It demonstrates how properties are declared and used.

Sample Files

See Properties Sample to download and build the sample files discussed in this tutorial.

Further Reading

  • Properties
  • Comparison Between Properties and Indexers
  • §10.6 Properties
  • §10.6.4 Abstract properties

Tutorial

This tutorial includes two examples. The first example shows how to declare and use read-write properties. The second example demonstrates abstract properties and shows how to override these properties in subclasses.

Example 1

This sample shows a Person class that has two properties: Name (string) and Age (int). Both properties are read-write.

000: // Properties\person.cs
001: using System;
002: class Person
003: {
004:     private string myName ="N/A";
005:     private int myAge = 0;
006:
007:     // Declare a Name property of type string
008:     public string Name
009:     {
010:         get
011:         {
012:            return myName;
013:         }
014:         set
015:         {
016:            myName = value;
017:         }
018:     }
019:
020:     // Declare an Age property of type int
021:     public int Age
022:     {
023:         get
024:         {
025:            return myAge;
026:         }
027:         set
028:         {
029:            myAge = value;
030:         }
031:     }
032:
033:     public override string ToString()
034:     {
035:         return "Name = " + Name + ", Age = " + Age;
036:     }
037:
038:     public static void Main()
039:     {
040:         Console.WriteLine("Simple Properties");
041:
042:         // Create a new Person Object
043:         Person person = new Person();
044:
045:         // Print out the name and the age associated with the person
046:         Console.WriteLine("Person details - {0}", person);
047:
048:         // Set some values on the person object
049:         person.Name = "Joe";
050:         person.Age = 99;
051:         Console.WriteLine("Person details - {0}", person);
052:
053:         // Increment the Age property
054:         person.Age += 1;
055:         Console.WriteLine("Person details - {0}", person);
056:     }
057: }

Output

Simple Properties
Person details - Name = N/A, Age = 0
Person details - Name = Joe, Age = 99
Person details - Name = Joe, Age = 100

Code Discussion

  • Notice the way that the Name and Age properties are declared in lines 008 – 031. The set and get methods of a property are contained inside the property declaration. You can control whether a property is read-write, read-only, or write-only by controlling whether a get or set method is included.
  • Once the properties are declared, they can be used as if they were fields of the class. This allows for a very natural syntax when both getting and setting the value of a property, as in lines 049, 050, and 054.
  • Note that in a property set method a special value variable is available. This variable contains the value that the user specified (see line 016 and line 029).
  • Line 054 increments the Age property on a Person object. Notice the clean syntax for doing this; if separate set and get methods were used to model properties, the equivalent code might look like this:
    person.SetAge(person.GetAge() + 1);
  • The ToString method, overridden in lines 033 – 036, is not explicitly used in the program. However, it is invoked by default by the WriteLine calls in lines 046, 051, and 055.

Example 2

The following example shows how to define abstract properties. An abstract property declaration does not provide an implementation of the property accessors. The example demonstrates how to override these properties in subclasses. It consists of three files that must be compiled together:

  • abstractshape.cs: The Shape class that contains an abstract Area property.
  • shapes.cs: The subclasses of the Shape class.
  • shapetest.cs: A test program to display the areas of some Shape-derived objects.

To compile the example, use the command line:

csc abstractshape.cs shapes.cs shapetest.cs

This will create the executable file shapetest.exe.

File 1 - abstractshape.cs

This file declares the Shape class that contains the Area property of the type double

000: // Properties\abstractshape.cs
001: using System;
002:
003: public abstract class Shape
004: {
005:    private string myId;
006:
007:    public Shape(string s)
008:    {
009:       Id = s;   // calling the set accessor of the Id property
010:    }
011:
012:    public string Id
013:    {
014:       get
015:       {
016:          return myId;
017:       }
018:
019:       set
020:       {
021:          myId = value;
022:       }
023:    }
024:
025:    public abstract double Area
026:    {
027:       get;
028:    }
029:
030:    public override string ToString()
031:    {
032:       return Id + " Area = " + double.Format(Area, "F");
033:    }
034: }

Code Discussion

  • Modifiers on the property are placed on the property declaration itself, for example, lines 012 and 025.
  • When declaring an abstract property (see line 025), you simply indicate what property accessors are available but do not implement them. In this example, only a get accessor is available, so the property is read-only.

File 2 - shapes.cs

The following code shows three subclasses of Shape and how they override the Area property to provide their own implementation.

000: // Properties\shapes.cs
001: public class Square : Shape
002: {
003:    private int mySide;
004:
005:    public Square(int side, string id) : base(id)
006:    {
007:       mySide = side;
008:    }
009:
010:    public override double Area
011:    {
012:       get
013:       {
014:          return mySide * mySide;
015:       }
016:    }
017: }
018:
019: public class Circle : Shape
020: {
021:    private int myRadius;
022:
023:    public Circle(int radius, string id) : base(id)
024:    {
025:       myRadius = radius;
026:    }
027:
028:    public override double Area
029:    {
030:       get
031:       {
032:          return myRadius * myRadius * System.Math.PI;
033:       }
034:    }
035: }
036:
037: public class Rectangle : Shape
038: {
039:    private int myWidth;
040:    private int myHeight;
041:
042:    public Rectangle(int width, int height, string id) : base(id)
043:    {
044:       myWidth  = width;
045:       myHeight = height;
046:    }
047:
048:    public override double Area
049:    {
050:       get
051:       {
052:          return myWidth * myHeight;
053:       }
054:    }
055: }

Code Discussion

  • The preceding code includes three implementations of the abstract property:
    • Line 014 gets the area of a square, given its side.
    • Line 032 gets the area of a circle, given its radius.
    • Line 052 gets the area of a rectangle, given its width and height.

File 3 - shapetest.cs

The following code shows a test program that creates a number of Shape-derived objects and prints out their areas.

000: // Properties\shapetest.cs
001: public class TestClass
002: {
003:    public static void Main()
004:    {
005:       Shape[] shapes =
006:          {
007:             new Square(5, "Square #1"),
008:             new Circle(3, "Circle #1"),
009:             new Rectangle( 4, 5, "Rectangle #1")
010:          };
011:
012:       System.Console.WriteLine("Shapes Collection");
013:       foreach(Shape s in shapes)
014:       {
015:          System.Console.WriteLine(s);
016:       }
017:
018:    }
019: }

Output

Shapes Collection
Square #1 Area = 25.00
Circle #1 Area = 28.27
Rectangle #1 Area = 20.00

See Also

C# Tutorials


Libraries Tutorial

This tutorial shows how to create and use libraries in C#.

Sample Files

See Libraries Sample to download and build the sample files discussed in this tutorial.

Further Reading

  • Creating and Using C# DLLs
  • C# Compiler Options

Tutorial

This tutorial demonstrates how to create a managed DLL file by using the necessary compiler options, and how to use the library by a client program.

Building a Library

The DLL library is built from the following two C# source files:

  • Factorial.cs: Calculates and returns the factorial of a number.
  • DigitCounter.cs: Counts the number of digits in the passed string.

To build the library, type the following at the command prompt:

csc /target:library /out:Functions.dll Factorial.cs DigitCounter.cs

where:

/target:library | Specifies that the output is a DLL and not an executable file (this also stops the compiler from looking for a default entry point).
/out:Functions.dll | Specifies that the output file name is Functions.dll. Normally the output name is the same name as the first C# source file on the command line (in this example, Factorial).
Factorial.cs DigitCounter.cs | The files to compile and place in the DLL.

Compiling the Client

The client program that uses the DLL is compiled from the source file:

  • FunctionClient.cs: Displays the factorial of the input arguments.

To compile the program, type the following at the command prompt:

csc /out:FunctionTest.exe /R:Functions.DLL FunctionClient.cs

where:

/out:FunctionTest.exe | Specifies that the output file name is FunctionTest.exe.
/R:Functions.DLL | Include Functions.DLL when resolving references. This DLL must be located in the current directory or have a fully qualified path.
FunctionClient.cs | The client source code.

This creates the executable file FunctionTest.exe.

Libraries Examples

Following are the listings of the three source files used in this program followed by code discussion for each file.

File 1 - Factorial.cs

The following code calculates the factorial of the integer passed to the method.

000: // Libraries\Factorial.cs
001: using System;
002:
003: namespace Functions
004: {
005:     public class Factorial
006:     {
007:         public static int Calc(int i)
008:         {
009:             return((i <= 1) ? 1 : (i * Calc(i-1)));
010:         }
011:     }
012: }

Code Discussion

  • Line 003 declares a namespace. Nearly all C# code will be organized into namespaces. You need to package your libraries according to their namespace so the .NET runtime can correctly load the classes.
  • Lines 005 – 011 declare the Factorial class that has one static method.
  • The Calc static method calculates the factorial value for the specified integer passed in.

File 2 - DigitCounter.cs

The following code is used to count the number of digit characters in the passed string:

000: // Libraries\DigitCounter.cs
001: using System;
002:
003: namespace Functions
004: {
005:     public class DigitCount
006:     {
007:         public static int NumberOfDigits(string theString)
008:         {
009:             int count = 0;
010:             for ( int i = 0; i < theString.Length; i++ )
011:             {
012:                 if ( Char.IsDigit(theString[i]) )
013:                 {
014:                       count++;
015:                 }
016:             }
017:
018:             return count;
019:         }
020:     }
021: }

Code Discussion

  • Line 003 declares a namespace. Note that this second file declares the same namespace as the one in Factorial.cs. This simply allows types to be added to the same namespace.
  • Lines 005 – 020 declare the DigitCount class that has one static method.
  • The NumberOfDigits static method calculates the number of digit characters in the passed string.

File 3 - FunctionClient.cs

Once you build a library, it can be used by other programs. The following client program uses the classes defined in the library. The basic function of the program is to take each command-line parameter and attempt to compute the factorial value for each argument.

000: // Libraries\FunctionClient.cs
001: using System;
002: using Functions;
003: class FunctionClient
004: {
005:     public static void Main(string[] args)
006:     {
007:         Console.WriteLine("Function Client");
008:
009:         if ( args.Length == 0 )
010:         {
011:             Console.WriteLine("Usage: FunctionTest ... ");
012:             return;
013:         }
014:
015:         for ( int i = 0; i < args.Length; i++ )
016:         {
017:             int num = Int32.Parse(args[i]);
018:             Console.WriteLine(
019:                "The Digit Count for String [{0}] is [{1}]",
020:                args[i],
021:                DigitCount.NumberOfDigits(args[i]));
022:             Console.WriteLine(
023:                "The Factorial for [{0}] is [{1}]",
024:                 num,
025:                 Factorial.Calc(num) );
026:         }
027:     }
028: }

Code Discussion

  • Line 002: the using directive makes the types defined in the Functions namespace available in this compilation unit.
  • Line 025 invokes the Calc static method in the Factorial class.
  • Line 021 invokes the NumberOfDigits static method in the DigitCount class.

Sample Run

The following command line uses the program FunctionTest to calculate the factorial of the three integers 3, 5, and 10. It also displays the number of digits for each argument.

FunctionTest 3 5 10

This run gives the output:

Function Client

The Digit Count for String [3] is [1]

The Factorial for [3] is [6]

The Digit Count for String [5] is [1]

The Factorial for [5] is [120]

The Digit Count for String [10] is [2]

The Factorial for [10] is [3628800]

Note To run the client executable (FunctionTest.exe), the file Functions.DLL must be in the current directory, a child directory, or in a directory specified by the CORPATH environment variable. If you change the name of the DLL, the program will not be able to load the class. CORPATH is an environment variable that the .NET runtime uses to look for classes.

See Also

C# Tutorials


Versioning Tutorial

This tutorial demonstrates versioning in C# through the use of the override and new keywords. Versioning maintains compatibility between base and derived classes as they evolve.

Sample Files

See Versioning Sample to download and build the sample files discussed in this tutorial.

Further Reading

  • virtual
  • override
  • new
  • §10.5.3 Virtual methods

Tutorial

The C# language is designed such that versioning between base and derived classes in different libraries can evolve and maintain backwards compatibility. This means, for example, that the introduction of a new member in a base class with the same name as a member in a derived class is not an error. It also means that a class must explicitly state whether a method is intended to override an inherited method, or whether a method is a new method that simply hides a similarly named inherited method.

In C#, methods are by default not virtual. To make a method virtual, the virtual modifier has to be used in the method declaration of the base class. The derived class can then override the base virtual method by using the override keyword or hide the virtual method in the base class by using the new keyword. If neither the override keyword nor the new keyword is specified, the compiler will issue a warning and the method in the derived class will hide the method in the base class. The following example shows these concepts in action.

Example

000: // Versioning\versioning.cs
001: public class MyBase
002: {
003:     public virtual string Meth1()
004:     {
005:         return "MyBase-Meth1";
006:     }
007:     public virtual string Meth2()
008:     {
009:         return "MyBase-Meth2";
010:     }
011:     public virtual string Meth3()
012:     {
013:         return "MyBase-Meth3";
014:     }
015: }
016:
017: class MyDerived : MyBase
018: {
019:     public override string Meth1()
020:     {
021:         return "MyDerived-Meth1";
022:     }
023:     public new string Meth2()
024:     {
025:         return "MyDerived-Meth2";
026:     }
027:     public string Meth3() // Issues a warning to alert the programmer
028:                           // that the method hides the inherited
029:                           // member MyBase.Meth3().
030:     {
031:         return "MyDerived-Meth3";
032:     }
033:
034:     public static void Main()
035:     {
036:         MyDerived mD = new MyDerived();
037:         MyBase mB = (MyBase) mD;
038:
039:         System.Console.WriteLine(mB.Meth1());
040:         System.Console.WriteLine(mB.Meth2());
041:         System.Console.WriteLine(mB.Meth3());
042:     }
043: }

Output

MyDerived-Meth1
MyBase-Meth2
MyBase-Meth3

Code Discussion

  • Line 019 overrides the virtual method Meth1 using the override keyword.
  • Line 023 explicitly hides the virtual method Meth3 using the new keyword.
  • Line 027 hides the virtual method Meth3 because a keyword isn't specified. A warning is issued to alert the programmer that a new or an override keyword should be specified.

Hiding a base class member from a derived class isn't an error in C#. This feature enables you to make changes in the base class without breaking other libraries that inherit this base class. For example, at some point you could have the following classes:

class Base {}
class Derived: Base
{
   public void F() {}
}

At some later point, the base class could evolve to add a void method F() as follows:

class Base
{
   public void F() {}
}
class Derived: Base
{
   public void F() {}
}

Thus, in C#, both the base and derived classes can evolve freely and maintain binary compatibility.

See Also

C# Tutorials


Collection Classes Tutorial

This tutorial shows how to implement a collection class that can be used with the foreach statement.

Sample Files

See Collection Classes Sample to download and build the sample files discussed in this tutorial.

Further Reading

  • foreach, in
  • §8.8.4 The foreach statement
  • Command Line Parameters Tutorial
  • Arrays Tutorial

Tutorial

The foreach statement is a convenient way to iterate over the elements of an array. It can also enumerate the elements of a collection, provided that the collection class has implemented the System.Collections.IEnumerator and System.Collections.IEnumerable interfaces.

Example 1

The following code sample illustrates how to write a collection class that can be used with foreach. The class is a string tokenizer, similar to the C run-time function strtok.

000: // CollectionClasses\tokens.cs
001: using System;
002: using System.Collections;
003:
004: public class Tokens : IEnumerable
005: {
006:    private string[] elements;
007:
008:    Tokens(string source, char[] delimiters)
009:    {
010:       elements = source.Split(delimiters);
011:    }
012:
013:    // IEnumerable Interface Implementation
014:
015:    public IEnumerator GetEnumerator()
016:    {
017:       return new TokenEnumerator(this);
018:    }
019:
020:    // Inner class implements IEnumerator interface
021:
022:    private class TokenEnumerator : IEnumerator
023:    {
024:       private int position = -1;
025:       private Tokens t;
026:
027:       public TokenEnumerator(Tokens t)
028:       {
029:          this.t = t;
030:       }
031:
032:       public bool MoveNext()
033:       {
034:          if (position < t.elements.Length - 1)
035:          {
036:             position++;
037:             return true;
038:          }
039:          else
040:          {
041:             return false;
042:          }
043:       }
044:
045:       public void Reset()
046:       {
047:          position = -1;
048:       }
049:
050:       public object Current
051:       {
052:          get
053:          {
054:             return t.elements[position];
055:          }
056:       }
057:    }
058:
059:    // Test Tokens, TokenEnumerator
060:
061:    static void Main()
062:    {
063:       Tokens f = new Tokens("This is a well-done program.", new char[] {' ','-'});
064:       foreach (string item in f)
065:       {
066:          Console.WriteLine(item);
067:       }
068:    }
069: }

Output

This
is
a
well
done
program.

Code Discussion

  • The .NET frameworks System namespace is made available on line 001.
  • The .NET frameworks System.Collections namespace is made available on line 002.
  • Line 004 declares the Tokens class.
  • Line 010 uses the String.Split method to parse the string into tokens.
  • Line 015 declares the GetEnumerator method required by IEnumerable. It returns a TokenEnumerator object.
  • Line 022 declares the inner class TokenEnumerator which implements the IEnumerator interface.
  • Line 032 declares the MoveNext method required by IEnumerator.
  • Line 045 declares the Reset method required by IEnumerator.
  • Line 050 declares the Current property required by IEnumerator.
  • Lines 063 – 067 test Tokens by breaking "This is a well-done program." into tokens (using ' ' and '-' as separators) and enumerating those tokens with the foreach statement on line 064.
Note Internally, Tokens uses an array, which implements IEnumerator and IEnumerable itself. The code sample could have leveraged the array's enumeration methods as its own, but that would have defeated the purpose of this example.

In C#, it is not strictly necessary for a collection class to inherit from IEnumerable and IEnumerator in order to be compatible with foreach; as long as the class has the required GetEnumerator, MoveNext, Reset, and Current members, it will work with foreach. Omitting the interfaces has the advantage of allowing you to define the return type of Current to be more specific than object, thereby providing type-safety.

For example, starting with the sample code above, change the following lines:

004: public class Tokens // no longer inherits from IEnumerable
015: public TokenEnumerator GetEnumerator() // doesn't return an IEnumerator
022: public class TokenEnumerator // no longer inherits from IEnumerator
050:       public string Current // type-safe: returns string, not object

Now, because Current returns a string, the compiler can detect when an incompatible type is used in a foreach statement:

064:       foreach (int item in f) // Error: cannot convert string to int

The disadvantage of omitting IEnumerable and IEnumerator is that the collection class is no longer interoperable with other .NET languages' foreach statements (or equivalents).

You can have the best of both worlds — type-safety within C# and interoperability with other .NET languages — by inheriting from IEnumerable and IEnumerator and using explicit interface implementation, as demonstrated in the following example.

Example 2

This sample is equivalent in function to Example 1, but it provides additional type-safety in C# while maintaining interoperability with other languages.

000: // CollectionClasses\tokens2.cs
001: using System;
002: using System.Collections;
003:
004: public class Tokens: IEnumerable
005: {
006:    private string[] elements;
007:
008:    Tokens(string source, char[] delimiters)
009:    {
010:       elements = source.Split(delimiters);
011:    }
012:
013:    // IEnumerable Interface Implementation
014:
015:    public TokenEnumerator GetEnumerator() // non-IEnumerable version
016:    {
017:       return new TokenEnumerator(this);
018:    }
019:
020:    IEnumerator IEnumerable.GetEnumerator() // IEnumerable version
021:    {
022:       return (IEnumerator) new TokenEnumerator(this);
023:    }
024:
025:    // Inner class implements IEnumerator interface
026:
027:    public class TokenEnumerator: IEnumerator
028:    {
029:       private int position = -1;
030:       private Tokens t;
031:
032:       public TokenEnumerator(Tokens t)
033:       {
034:          this.t = t;
035:       }
036:
037:       public bool MoveNext()
038:       {
039:          if (position < t.elements.Length - 1)
040:          {
041:             position++;
042:             return true;
043:          }
044:          else
045:          {
046:             return false;
047:          }
048:       }
049:
050:       public void Reset()
051:       {
052:          position = -1;
053:       }
054:
055:       public string Current // non-IEnumerator version: type-safe
056:       {
057:          get
058:          {
059:             return t.elements[position];
060:          }
061:       }
062:
063:       object IEnumerator.Current // IEnumerator version: returns object
064:       {
065:          get
066:          {
067:             return t.elements[position];
068:          }
069:       }
070:    }
071:
072:    // Test Tokens, TokenEnumerator
073:
074:    static void Main()
075:    {
076:       Tokens f = new Tokens("This is a well-done program.", new char [] {' ','-'});
077:       foreach (string item in f) // try changing string to int
078:       {
079:          Console.WriteLine(item);
080:       }
081:    }
082: }

See Also

C# Tutorials


Structs Tutorial

This tutorial presents the syntax and usage of structs. It also covers the important differences between classes and structs.

Sample Files

See Structs Sample to download and build the sample files discussed in this tutorial.

Further Reading

  • struct
  • §1.8 Structs
  • §21.11 Structs Grammar
  • Value Types
  • C# Attributes
  • §17. Attributes
  • 19.20 The StructLayout attribute

Tutorial

This tutorial includes two examples. The first one shows you how to declare and use structs, and the second one demonstrates the difference between structs and classes when instances are passed to methods. You are also introduced to the following topics:

  • Structs vs. Classes
  • Heap or Stack?
  • Constructors and Inheritance
  • Attributes on Structs

Example 1

This example declares a struct with three members: a property, a method, and a private field. It creates an instance of the struct and puts it to use:

000: // Structs\struct1.cs
001: using System;
002: struct SimpleStruct
003: {
004:     private int xval;
005:     public int X
006:     {
007:         get {
008:             return xval;
009:         }
010:         set {
011:             if (value < 100)
012:                 xval = value;
013:         }
014:     }
015:     public void DisplayX()
016:     {
017:         Console.WriteLine("The stored value is: {0}", xval);
018:     }
019: }
020:
021: class TestClass
022: {
023:     public static void Main()
024:     {
025:         SimpleStruct ss = new SimpleStruct();
026:         ss.X = 5;
027:         ss.DisplayX();
028:     }
029: }

Output

The stored value is: 5

Structs vs. Classes

Structs may seem similar to classes, but there are important differences that you should be aware of. First of all, classes are reference types and structs are value types. By using structs, you can create objects that behave like the built-in types and enjoy their benefits as well.

Heap or Stack?

When you call the new operator on a class, it will be allocated on the heap. However, when you instantiate a struct, it gets created on the stack. This will yield performance gains. Also, you will not be dealing with references to an instance of a struct, as you would with classes. You will be working directly with the struct instance. Because of this, when passing a struct to a method, it's passed by value instead of as a reference.

Example 2

This example shows that when a struct is passed to a method, a copy of the struct is passed, but when a class instance is passed, a reference is passed.

000: // Structs\struct2.cs
001: using System;
002:
003: class TheClass
004: {
005:     public int x;
006: }
007:
008: struct TheStruct
009: {
010:     public int x;
011: }
012:
013: class TestClass
014: {
015:     public static void structtaker(TheStruct s)
016:     {
017:         s.x = 5;
018:     }
019:     public static void classtaker(TheClass c)
020:     {
021:         c.x = 5;
022:     }
023:     public static void Main()
024:     {
025:         TheStruct a = new TheStruct();
026:         TheClass b = new TheClass();
027:         a.x = 1;
028:         b.x = 1;
029:         structtaker(a);
030:         classtaker(b);
031:         Console.WriteLine("a.x = {0}", a.x);
032:         Console.WriteLine("b.x = {0}", b.x);
033:     }
034: }

Output

a.x = 1
b.x = 5

Code Discussion

The output of the example shows that only the value of the class field was changed when the class instance was passed to the classtaker method. The struct field, however, did not change by passing its instance to the structtaker method. This is because a copy of the struct was passed to the structtaker method, while a reference to the class was passed to the classtaker method.

Constructors and Inheritance

Structs can declare constructors, but they must take parameters. It is an error to declare a default (parameterless) constructor for a struct. Struct members cannot have initializers. A default constructor is always provided to initialize the struct members to their default values.

When you create a struct object using the new operator, it gets created and the appropriate constructor is called. Unlike classes, structs can be instantiated without using the new operator. If you do not use new, the fields will remain unassigned and the object cannot be used until all the fields are initialized.

There is no inheritance for structs as there is for classes. A struct cannot inherit from another struct or class, and it cannot be the base of a class. Structs, however, inherit from the base class object. A struct can implement interfaces, and it does that exactly as classes do. Here's a code snippet of a struct implementing an interface:

interface IImage
{
    void Paint();
}

struct Picture : IImage
{
    public void Paint()
    {
         // painting code goes here
    }
    private int x, y, z;  // other struct members
}

Attributes on Structs

By using attributes you can customize how structs are laid out in memory. For example, you can create what's known as a union in C/C++ by using the StructLayout(LayoutKind.Union) attribute.

using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Union)]
struct TestUnion
{
    public int i;
    public double d;
    public char c;
    public byte b1;
}

The fields of testUnion all start at the same location in memory.

You can also explicitly set the offsets of each struct field by using the StructLayout(LayoutKind.Explicit) and FieldOffset attributes. Here's an example:

using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Explicit)]
struct TestExplicit
{
    [FieldOffset(0)]
    public long lg;
    [FieldOffset(0)]
    public int i1;
    [FieldOffset(4)]
    public int i2;
    [FieldOffset(8)]
    public double d;
    [FieldOffset(12)]
    public char c;
    [FieldOffset(14)]
    public byte b1;
}

The two int fields, i1 and i2, share the same memory locations as lg. This sort of control over struct layout is useful when using platform invocation.

Conclusion

Structs are simple to use and can prove to be useful at times. Just keep in mind that they're created on the stack and that you're not dealing with references to them but dealing directly with them. Whenever you have a need for a type that will be used often and is mostly just a piece of data, structs might be a good option.

See Also

C# Tutorials


Indexers Tutorial

This tutorial shows how C# classes can declare indexers to provide array-like access to the classes.

Sample Files

See Indexers Sample to download and build the sample files discussed in this tutorial.

Further Reading

  • Indexers
  • Accessors
  • Comparison Between Properties and Indexers
  • §10.8 Indexers
  • 10.8.1 Indexer overloading
  • 10.8.3 Override indexers

Tutorial

Defining an indexer allows you to create classes that act like "virtual arrays." Instances of that class can be accessed using the [] array access operator. Defining an indexer in C# is similar to defining operator[] in C++, but is considerably more flexible. For classes that encapsulate array- or collection-like functionality, using an indexer allows the users of that class to use the array syntax to access the class.

For example, suppose you want to define a class that makes a file appear like an array of bytes. If the file was very large, it would be impractical to read the entire file into memory, especially if you only wanted to read or change a few bytes. By defining a FileByteArray class, you could make the file appear similar to an array of bytes, but actually do file input and output when a byte was read or written.

In addition to the example below, an advanced topic on Creating an Indexed Property is discussed in this tutorial.

Example

In this example, the class FileByteArray makes it possible to access a file as if it were a byte array. The Reverse class reverses the bytes of the file. You can run this program to reverse the bytes of any text file including the program source file itself. To change the reversed file back to normal, run the program on the same file again.

000: // Indexers\indexer.cs
001: using System;
002: using System.IO;
003:
004: // Class to provide access to a large file
005: // as if it were a byte array.
006: public class FileByteArray
007: {
008:     Stream stream;      // Holds the underlying stream
009:                         // used to access the file.
010: // Create a new FileByteArray encapsulating a particular file.
011:     public FileByteArray(string fileName)
012:     {
013:         stream = new FileStream(fileName, FileMode.Open);
014:     }
015:
016:     // Close the stream. This should be the last thing done
017:     // when you are finished.
018:     public void Close()
019:     {
020:         stream.Close();
021:         stream = null;
022:     }
023:
024:     // Indexer to provide read/write access to the file.
025:     public byte this[long index]   // long is a 64-bit integer
026:     {
027:         // Read one byte at offset index and return it.
028:         get
029:         {
030:             byte[] buffer = new byte[1];
031:             stream.Seek(index, SeekOrigin.Begin);
032:             stream.Read(buffer, 0, 1);
033:             return buffer[0];
034:         }
035:         // Write one byte at offset index and return it.
036:         set
037:         {
038:             byte[] buffer = new byte[1] {value};
039:             stream.Seek(index, SeekOrigin.Begin);
040:             stream.Write(buffer, 0, 1);
041:         }
042:     }
043:
044:     // Get the total length of the file.
045:     public long Length
046:     {
047:         get {
048:             return stream.Seek(0, SeekOrigin.End);
049:         }
050:     }
051: }
052:
053: // Demonstrate the FileByteArray class.
054: // Reverses the bytes in a file.
055: public class Reverse
056: {
057:     public static void Main(String[] args)
058:     {
059:         // Check for arguments.
060:         if (args.Length == 0)
061:         {
062:             Console.WriteLine("indexer <filename>");
063:             return;
064:         }
065:
066:         FileByteArray file = new FileByteArray(args[0]);
067:         long len = file.Length;
068:
069:         // Swap bytes in the file to reverse it.
070:         for (long i = 0; i < len / 2; ++i)
071:         {
072:             byte t;
073:
074:             // Note that indexing the "file" variable invokes the
075:             // indexer on the FileByteStream class, which reads
076:             // and writes the bytes in the file.
077:             t = file[i];
078:             file[i] = file[len - i - 1];
079:             file[len - i - 1] = t;
080:         }
081:
082:         file.Close();
083:     }
084: }

Sample Run

To test the program you can use the file Test.txt, which is included in the same folder with the sample (or create a new file to test). This is the text of the file:

000: // Indexers\Test.txt

001: public class Hello1

002: {

003:     public static void Main()

004:     {

005:         System.Console.WriteLine("Hello, World!");

006:     }

007: }

To reverse the bytes of this file, compile the program and then use the command line:

indexer Test.txt

To display the reversed file, enter the command:

Type Test.txt

The following output will be displayed:

}
}
;)"!dlroW ,olleH"(eniLetirW.elosnoC.metsyS
{
)(niaM diov citats cilbup
{
1olleH ssalc cilbup
txt.tseT\srexednI //

Code Discussion

  • Since an indexer is accessed using the [] operator, it does not have a name. For indexer declaration syntax, see Indexers.
  • In the example above, the indexer is of type byte and takes a single index of type long (64-bit integer). The get accessor defines the code to read a byte from the file, while the set accessor defines the code to write a byte to the file. Inside the set accessor, the predefined parameter value has the value that is being assigned to the virtual array element.
  • An indexer must have at least one parameter. Although it is comparatively rare, an indexer can have more than one parameter, in order to simulate a multidimensional "virtual array." Although integral parameters are the most common, the indexer parameter can be of any type. For example, the standard Dictionary class provides an indexer with a parameter of type object.
  • Although indexers are a powerful feature, it is important to use them only when the array-like abstraction makes sense. Always carefully consider whether using regular method(s) would be just as clear. For example, the following is a bad use of an indexer:
    class Employee
{
    // VERY BAD STYLE: using an indexer to access
    // the salary of an employee.
    public double this[int year]
   {
        get
        {
            // return employee's salary for a given year.
        }
   }
}

    Although legal, an indexer with only a get accessor is rarely good style. Strongly consider using a method in this case.

  • Indexers can be overloaded (for more information, see §10.8.1 Indexer overloading) and overridden (for more information, see §10.8.3 Override indexers).

Advanced Topic: Creating an "Indexed Property"

Occasionally, a class represents an abstraction that can be viewed as an array-like collection of several different kinds of things. In these rare cases, a technique known as "indexed properties" can be used to provide an abstraction for using the class.

For example, suppose you want to write a class Document that encapsulates a lengthy section of text. To allow easy implementation of operations such as checking spelling, you might want to view the document as a "virtual array" of words or sentences. You want to be able to write code like the following to change words or sentences in the document.

Document d = new Document();
// ...
for (int i = 0; i < d.Words.Count; ++i)
{
    if (d.Words[i] == "Peter")
        d.Words[i] = "Joe";
}
for (int i = 0; i < d.Sentences.Count; ++i)
{
    if (d.Sentences[i] == "Elvis is the king.")
        d.Sentences[i] = "Eric Clapton is a guitar god.";
}

The following example shows a technique for implementing such a class. For each "indexed property," you define a nested class or struct, which contains a reference back to the main class instance. A readonly field on the main class provides access to an instance of the nested class or struct that defines each "virtual array."

public class Document
{
    public struct WordCollection
    {
        readonly Document document;  // The containing document

        internal WordCollection(Document d)
        {
             document = d;
         }

        public string this[int indexer]
        {
            get
            {
                return document.GetNthWord(indexer);
            }
            set
            {
                document.SetNthWord(indexer, value);
            }
        }
        public int Count
        {
            get
            {
                return document.CountWords();
            }
        }
    }

    public struct SentenceCollection
    {
        readonly Document document;  // The containing document

        internal SentenceCollection(Document d)
        {
            document = d;
        }

        public string this[int indexer] {
            get
            {
                return document.GetNthSentence(indexer);
            }
            set
            {
                document.SetNthSentence(indexer, value);
            }
        }
        public int Count
        {
            get
            {
                return document.CountSentences();
            }
        }
    }

    // Because the types of the fields have indexers,
    // these fields appear as "indexed properties"
    public readonly WordCollection Words;
    public readonly SentenceCollection Sentences;

    public Document()
    {
        Words = new WordCollection(this);
        Sentences = new SentenceCollection(this);
    }

    private string GetNthWord(int index)
    {
         /* ... */
         return "";
    }
    private void SetNthWord(int index, string w)
    {
         /* ... */
    }
    private int CountWords()
    {
         /* ... */
         return 0;
    }
    private string GetNthSentence(int index)
    {
         /* ... */
         return "";
    }
    private void SetNthSentence(int index, string s)
    {
         /* ... */
    }
    private int CountSentences()
    {
         /* ... */
         return 0;
    }
}

Use this technique sparingly! The caution about only using indexers when the array abstraction makes sense applies with even more force when using "indexed properties." Only use this pattern if the abstraction provided by using array indexing operations significantly clarifies code that uses your class, and if the indexers have both get and set accessors.

See Also

C# Tutorials


User-Defined Conversions Tutorial

This tutorial shows how to define and use conversions to or from classes or structs.

Sample Files

See User-Defined Conversions Sample to download and build the sample files discussed in this tutorial.

Further Reading

  • explicit
  • implicit
  • () Operator
  • Implicit Numeric Conversions Table
  • operator
  • §6.4 User-defined conversions

Tutorial

C# allows programmers to declare conversions on classes or structs, so that classes or structs can be converted to and/or from other classes or structs, or basic types. Conversions are defined like operators and are named for the type to which they convert.

In C#, conversions can be declared either as implicit, which occur automatically when required, or explicit, which require a cast to be called. All conversions must be static, and must either take the type the conversion is defined on, or return that type.

This tutorial introduces two examples. The first one shows you how to declare and use conversions, and the second one demonstrates conversions between structs.

Example 1

In this example, a RomanNumeral type is declared, and several conversions to and from it are defined.

000: // UserConversions\conversion.cs
001: using System;
002:
003: struct RomanNumeral
004: {
005:     public RomanNumeral(int value)
006:     {
007:        this.value = value;
008:     }
009:     static public implicit operator RomanNumeral(int value)
010:     {
011:        return new RomanNumeral(value);
012:     }
013:     static public explicit operator int(RomanNumeral roman)
014:     {
015:        return roman.value;
016:     }
017:     static public implicit operator string(RomanNumeral roman)
018:     {
019:        return("Conversion not yet implemented");
020:     }
021:     private int value;
022: }
023:
024: class Test
025: {
026:     static public void Main()
027:     {
028:         RomanNumeral numeral;
029:
030:         numeral = 10;
031:
032: // Explicit conversion from numeral to int:
033:         Console.WriteLine((int)numeral);
034:
035: // Implicit conversion to string:
036:         Console.WriteLine(numeral);
037:
038: // Explicit conversion from numeral to int
039: // and explicit conversion from int to short:
040:         short s = (short)numeral;
041:
042:         Console.WriteLine(s);
043:
044:     }
045: }

Output

10
Conversion not yet implemented
10

Code Discussion

  • Line 009 declares a conversion from an int to a RomanNumeral. Note the use of the operator keyword — this is a conversion operator named RomanNumeral.
  • Line 011: Note that because RomanNumeral is declared as a struct, calling new on the struct merely calls the constructor rather than allocating an object on the heap.
  • Line 013 declares an explicit conversion from a RomanNumeral to an int.
  • Line 017 declares an implicit conversion from a RomanNumeral to a string.
  • Line 033 calls the explicit conversion to convert the numeral to an int. Because it is an explicit conversion, a cast must be used.
  • Line 036 calls the conversion to string. Because there is no cast, the implicit conversion to string is the only conversion that is considered.
  • Line 040 calls the explicit conversion from numeral to int, and then the explicit numeric conversion from int to short. For more information, see §6.4.4 User-defined explicit conversions.

Example 2

This example defines two structs, RomanNumeral and BinaryNumeral, and demonstrates conversions between them.

000: // UserConversions\structconversion.cs
001: using System;
002:
003: struct RomanNumeral
004: {
005:     public RomanNumeral(int value) { this.value = value; }
006:     static public implicit operator RomanNumeral(int value)
007:         {return new RomanNumeral(value);}
008:     static public implicit operator
009:         RomanNumeral(BinaryNumeral binary)
010:             {return new RomanNumeral((int)binary);}
011:     static public explicit operator int(RomanNumeral roman)
012:         {return roman.value;}
013:     static public implicit operator string(RomanNumeral roman)
014:         {return("Conversion not yet implemented");}
015:     private int value;
016: }
017:
018: struct BinaryNumeral
019: {
020:     public BinaryNumeral(int value) {this.value = value;}
021:
022:     static public implicit operator BinaryNumeral(int value)
023:         {return new BinaryNumeral(value);}
024:     static public implicit operator string(BinaryNumeral binary)
025:         {return("Conversion not yet implemented");}
026:     static public explicit operator int(BinaryNumeral binary)
027:         {return(binary.value);}
028:
029:     private int value;
030: }
031:
032: class Test
033: {
034:     static public void Main()
035:     {
036:         RomanNumeral roman;
037:         roman = 10;
038:         BinaryNumeral binary;
039:         binary = (BinaryNumeral)(int)roman;
040:         roman = binary;
041:         Console.WriteLine((int)binary);
042:         Console.WriteLine(binary);
043:     }
044: }

Output

10
Conversion not yet implemented

Code Discussion

  • Line 039 performs a conversion from a RomanNumeral to a BinaryNumeral. Because there is no direct conversion from RomanNumeral to BinaryNumeral, a cast is used to convert from a RomanNumeral to an int, and another cast to convert from an int to a BinaryNumeral.
  • Line 040 performs a conversion from a RomanNumeral to a BinaryNumeral. Because RomanNumeral defines an implicit conversion from BinaryNumeral, no cast is required.

See Also

C# Tutorials


Operator Overloading Tutorial

This tutorial demonstrates how user-defined classes can overload operators.

Sample Files

See Operator Overloading Sample to download and build the sample files discussed in this tutorial.

Further Reading

  • §7.2.2 Operator overloading
  • C# Operators
  • Overloadable Operators
  • true
  • false
  • bool

Tutorial

Operator overloading permits user-defined operator implementations to be specified for operations where one or both of the operands are of a user-defined class or struct type. The tutorial contains two examples. The first one shows how to use operator overloading to create a complex number class that defines complex addition. The second example shows how to use operator overloading to implement a three-valued logical type.

Example 1

This example shows how you can use operator overloading to create a complex number class Complex that defines complex addition. The program displays the imaginary and the real parts of the addition result.

000: // OperatorOverloading\complex.cs
001: using System;
002:
003: public class Complex
004: {
005:    public int real = 0;
006:    public int imaginary = 0;
007:
008:    public Complex(int real, int imaginary)
009:    {
010:       this.real = real;
011:       this.imaginary = imaginary;
012:    }
013:
014:    public static Complex operator +(Complex c1, Complex c2)
015:    {
016:       return new Complex(c1.real + c2.real, c1.imaginary + c2.imaginary);
017:    }
018:
019:    public static void Main()
020:    {
021:       Complex num1 = new Complex(2,3);
022:       Complex num2 = new Complex(3,4);
023:
024:       Complex sum = num1 + num2;
025:
026:       Console.WriteLine("Real: {0}", sum.real);
027:       Console.WriteLine("Imaginary: {0}", sum.imaginary);
028:    }
029: }

Output

Real: 5
Imaginary: 7

Code Discussion

  • Line 014 declares which operator to overload (+), the types that can be added (two Complex objects), and the return type (Complex).
  • Line 024 adds two Complex objects (num1 and num2) through the overloaded plus operator declared on line 014.
  • Lines 026 and 027 show on execution that the real and imaginary parts of objects num1 and num2 have been added to produce the sum values.

Example 2

This example shows how operator overloading can be used to implement a three-valued logical type. The possible values of this type are DBBool.dbTrue, DBBool.dbFalse, and DBBool.dbNull, where the dbNull member indicates an unknown value.

000: // OperatorOverloading\dbbool.cs
001: using System;
002:
003: public struct DBBool
004: {
005:    // The three possible DBBool values.
006:    public static readonly DBBool dbNull = new DBBool(0);
007:    public static readonly DBBool dbFalse = new DBBool(-1);
008:    public static readonly DBBool dbTrue = new DBBool(1);
009:    // Private field that stores -1, 0, 1 for dbFalse, dbNull, dbTrue.
010:    int value;
011:
012:    // Private constructor. The value parameter must be -1, 0, or 1.
013:    DBBool(int value)
014:    {
015:       this.value = value;
016:    }
017:
018:    // Implicit conversion from bool to DBBool. Maps true to
019:    // DBBool.dbTrue and false to DBBool.dbFalse.
020:    public static implicit operator DBBool(bool x)
021:    {
022:       return x? dbTrue: dbFalse;
023:    }
024:
025:    // Explicit conversion from DBBool to bool. Throws an
026:    // exception if the given DBBool is dbNull, otherwise returns
027:    // true or false.
028:    public static explicit operator bool(DBBool x)
029:    {
030:       if (x.value == 0) throw new InvalidOperationException();
031:       return x.value > 0;
032:    }
033:
034:    // Equality operator. Returns dbNull if either operand is dbNull,
035:    // otherwise returns dbTrue or dbFalse.
036:    public static DBBool operator ==(DBBool x, DBBool y)
037:    {
038:       if (x.value == 0 || y.value == 0) return dbNull;
039:       return x.value == y.value? dbTrue: dbFalse;
040:    }
041:
042:    // Inequality operator. Returns dbNull if either operand is dbNull,
043:    // otherwise returns dbTrue or dbFalse.
044:    public static DBBool operator !=(DBBool x, DBBool y)
045:    {
046:       if (x.value == 0 || y.value == 0) return dbNull;
047:       return x.value != y.value? dbTrue: dbFalse;
048:    }
049:
050:    // Logical negation operator. Returns dbTrue if the operand is
051:    // dbFalse, dbNull if the operand is dbNull, or dbFalse if the operand
052:    // is dbTrue.
053:    public static DBBool operator !(DBBool x)
054:    {
055:       return new DBBool(-x.value);
056:    }
057:
058:    // Logical AND operator. Returns dbFalse if either operand is
059:    // dbFalse, dbNull if either operand is dbNull, otherwise dbTrue.
060:    public static DBBool operator &(DBBool x, DBBool y)
061:    {
062:       return new DBBool(x.value < y.value? x.value: y.value);
063:    }
064:
065:    // Logical OR operator. Returns dbTrue if either operand is dbTrue,
066:    // dbNull if either operand is dbNull, otherwise dbFalse.
067:    public static DBBool operator |(DBBool x, DBBool y)
068:    {
069:       return new DBBool(x.value > y.value? x.value: y.value);
070:    }
071:
072:    // Definitely true operator. Returns true if the operand is
073:    // dbTrue, false otherwise.
074:    public static bool operator true(DBBool x)
075:    {
076:       return x.value > 0;
077:    }
078:
079:    // Definitely false operator. Returns true if the operand is
080:    // dbFalse, false otherwise.
081:    public static bool operator false(DBBool x)
082:    {
083:       return x.value < 0;
084:    }
085:
086:    // Overloading the conversion from DBBool to string.
087:    public static implicit operator string(DBBool x)
088:    {
089:       return x.value > 0 ? "dbTrue"
090:            : x.value < 0 ? "dbFalse"
091:            : "dbNull";
092:    }
093:
094:    // Override the ToString method to convert DBBool to a string.
095:    public override string ToString()
096:    {
097:       return (string)this;
098:    }
099: }
100:
101: class Test
102: {
103:    static void Main()
104:    {
105:       DBBool a, b;
106:       a = DBBool.dbTrue;
107:       b = DBBool.dbNull;
108:
109:       Console.WriteLine( "!{0} = {1}", a, !a);
110:       Console.WriteLine( "!{0} = {1}", b, !b);
111:       Console.WriteLine( "{0} & {1} = {2}", a, b, a & b);
112:       Console.WriteLine( "{0} | {1} = {2}", a, b, a | b);
113:       if (b)
114:          Console.WriteLine("b is definitely true");
115:       else
116:          Console.WriteLine("b is not definitely true");
117:    }
118: }

Output

!dbTrue = dbFalse
!dbNull = dbNull
dbTrue & dbNull = dbNull
dbTrue | dbNull = dbTrue
b is not definitely true

Code Discussion

  • Line 020 defines an implicit conversion operator from bool to DBBool.
  • Line 028 defines an explicit conversion operator from DBBool to bool.
  • Lines 074 – 084 define true and false operators; these are used to determine the value in a Boolean expression. In line 113, the true operator is invoked to determine the Boolean value of the DBBool variable.
  • Lines 094 – 098 define an override to the ToString method to convert DBBool to a string.
  • Notice that defining the true and false operators is only useful for types that represent true, false, and null (neither true nor false), as used in databases.

See Also

C# Tutorials


Delegates Tutorial

This tutorial demonstrates the delegate types. It shows how to map delegates to static and instance methods, and how to combine them (multicast).

Sample Files

See Delegates Sample to download and build the sample files discussed in this tutorial.

Further Reading

  • delegate
  • §4.2.6 Delegate types
  • Events Tutorial

Tutorial

A delegate in C# is similar to a function pointer in C or C++. Using a delegate allows the programmer to encapsulate a reference to a method inside a delegate object. The delegate object can then be passed to code which can call the referenced method, without having to know at compile time which method will be invoked. Unlike function pointers in C or C++, delegates are object-oriented, type-safe, and secure.

A delegate declaration defines a type that encapsulates a method with a particular set of arguments and return type. For static methods, a delegate object encapsulates the method to be called. For instance methods, a delegate object encapsulates both an instance and a method on the instance. If you have a delegate object and an appropriate set of arguments, you can invoke the delegate with the arguments.

An interesting and useful property of a delegate is that it does not know or care about the class of the object that it references. Any object will do; all that matters is that the method's argument types and return type match the delegate's. This makes delegates perfectly suited for "anonymous" invocation.

This tutorial includes two examples:

  • Example 1 shows how to declare, instantiate, and call a delegate.
  • Example 2 shows how to combine two delegates.

In addition, it discusses the following topics:

  • Delegates and Events
  • Delegates vs. Interfaces

Example 1

The following example illustrates declaring, instantiating, and using a delegate. The BookDB class encapsulates a bookstore database that maintains a database of books. It exposes a method ProcessPaperbackBooks, which finds all paperback books in the database and calls a delegate for each one. The delegate type used is called ProcessBookDelegate. The Test class uses this class to print out the titles and average price of the paperback books.

The use of delegates promotes good separation of functionality between the bookstore database and the client code. The client code has no knowledge of how the books are stored or how the bookstore code finds paperback books. The bookstore code has no knowledge of what processing is done on the paperback books after it finds them.

000: // Delegates\bookstore.cs
001: using System;
002:
003: // A set of classes for handling a bookstore.
004: namespace Bookstore
005: {
006:   using System.Collections;
007:
008:   // Describes a book in the book list.
009:   public struct Book
010:   {
011:     public string Title;        // Title of the book.
012:     public string Author;       // Author of the book.
013:     public decimal Price;       // Price of the book.
014:     public bool Paperback;      // Is it paperback?
015:
016:     public Book(string title, string author, decimal price, bool paperBack)
017:     {
018:       Title = title;
019:       Author = author;
020:       Price = price;
021:       Paperback = paperBack;
022:     }
023:   }
024:
025:   // Declare a delegate type for processing a book.
026:   public delegate void ProcessBookDelegate(Book book);
027:
028:   // Maintains a book database.
029:   public class BookDB
030:   {
031:     ArrayList list = new ArrayList();         // list of all books in the database.
032:
033:     // Add a book to the database.
034:     public void AddBook(string title, string author, decimal price, bool paperBack)
035:     {
036:       list.Add(new Book(title, author, price, paperBack));
037:     }
038:
039:     // Call a passed in delegate on each paperback book to process it.
040:     public void ProcessPaperbackBooks(ProcessBookDelegate processBook)
041:     {
042:       foreach (Book b in list)
043:       {
044:         if (b.Paperback)
045:           processBook(b);
046:       }
047:     }
048:   }
049: }
050:
051: // Using the Bookstore classes.
052: namespace BookTestClient
053: {
054:   using Bookstore;
055:
056:   // Class to total and average prices of books.
057:   class PriceTotaller
058:   {
059:     int countBooks = 0;
060:     decimal priceBooks = 0.0m;
061:
062:     internal void AddBookToTotal(Book book)
063:     {
064:       countBooks += 1;
065:       priceBooks += book.Price;
066:     }
067:
068:     internal decimal AveragePrice()
069:     {
070:       return priceBooks / countBooks;
071:     }
072:   }
073:
074:   // Class to test the book database.
075:   class Test
076:   {
077:     // Print the title of the book.
078:     static void PrintTitle(Book b)
079:     {
080:       Console.WriteLine("   {0}", b.Title);
081:     }
082:
083:     // Execution starts here.
084:     static void Main()
085:     {
086:       BookDB bookDB = new BookDB();
087:
088:       // Initialize the database with some books.
089:       AddBooks(bookDB);
090:
091:       // Print all the titles of paperbacks
092:       Console.WriteLine("Paperback Book Titles:");
093:       bookDB.ProcessPaperbackBooks(new ProcessBookDelegate(PrintTitle));
094:
095:       // Get the average price of a paperback by using a PriceTotaller object.
096:       PriceTotaller totaller = new PriceTotaller();
097:       bookDB.ProcessPaperbackBooks(new ProcessBookDelegate(totaller.AddBookToTotal));
098:       Console.WriteLine("Average Paperback Book Price: ${0:#.##}", totaller.AveragePrice());
099:     }
100:
101:     // Initialize the book database with some test books.
102:     static void AddBooks(BookDB bookDB)
103:     {
104:       bookDB.AddBook("The C Programming Language", "Brian W. Kernighan and Dennis M. Ritchie", 19.95m, true);
105:       bookDB.AddBook("The Unicode Standard 2.0", "The Unicode Consortium", 39.95m, true);
106:       bookDB.AddBook("The MS-DOS Encyclopedia", "Ray Duncan", 129.95m, false);
107:       bookDB.AddBook("Dogbert's Clues for the Clueless", "Scott Adams", 12.00m, true);
108:     }
109:   }
110: }

Output

Paperback Book Titles:
   The C Programming Language
   The Unicode Standard 2.0
   Dogbert's Clues for the Clueless
Average Paperback Book Price: $23.97

Code Discussion

  • Declaring a delegate: Line 026 declares a new delegate type. Each delegate type describes the number and types of the arguments, and the type of the return value, of methods that it can encapsulate. Whenever a new set of argument types or return value type is needed, a new delegate type must be declared.
  • Instantiating a delegate: Once a delegate type has been declared, a delegate object must be created and associated with a particular method. Like all other objects, a new delegate object is created with a new expression. When creating a delegate, however, the argument passed to the new expression is special — it is written like a method call, but without the arguments to the method.

    Line 093 creates a new delegate object associated with the static method Test.PrintTitle. Line 097 creates a new delegate object associated with the nonstatic method AddBookToTotal on the object totaller. In both cases, this new delegate object is immediately passed to the ProcessPaperbackBooks method.

    Note that once a delegate is created, the method it is associated with never changes — delegate objects are immutable.

  • Calling a delegate: Once a delegate object is created, the delegate object is typically passed to other code that will call the delegate. A delegate object is called by using the name of the delegate object, followed by the parenthesized arguments to be passed to the delegate. Line 045 shows an example of a delegate call.

Example 2

This example demonstrates composing delegates. A useful property of delegate objects is that they can be composed using the "+" operator. A composed delegate calls, in order, the two delegates it was composed from. Only delegates of the same type can be composed, and the delegate type must have a void return value. The "-" operator can be used to remove a component delegate from a composed delegate.

000: // Delegates\compose.cs
001: using System;
002:
003: delegate void MyDelegate(string s);
004:
005: class MyClass
006: {
007:     public static void Hello(string s)
008:     {
009:         Console.WriteLine("  Hello, {0}!", s);
010:     }
011:
012:     public static void Goodbye(string s)
013:     {
014:         Console.WriteLine("  Goodbye, {0}!", s);
015:     }
016:
017:     public static void Main()
018:     {
019:         MyDelegate a, b, c, d;
020:
021:         a = new MyDelegate(Hello);
022:         b = new MyDelegate(Goodbye);
023:         c = a + b;   // Compose two delegates to make another
024:         d = c - a;   // Remove a from the composed delegate
025:
026:         Console.WriteLine("Invoking delegate a:");
027:         a("A");
028:         Console.WriteLine("Invoking delegate b:");
029:         b("B");
030:         Console.WriteLine("Invoking delegate c:");
031:         c("C");
032:         Console.WriteLine("Invoking delegate d:");
033:         d("D");
034:     }
035: }

Output

Invoking delegate a:
  Hello, A!
Invoking delegate b:
  Goodbye, B!
Invoking delegate c:
  Hello, C!
  Goodbye, C!
Invoking delegate d:
  Goodbye, D!

Code Discussion

  • Line 021 creates the delegate object a, which references the method Hello.
  • Line 022 creates the delegate object b, which references the method Goodbye.
  • In line 023, these two delegates are composed to form c, which calls both methods in order.
  • Line 024 removes a from the composed delegate, leaving d, which calls only the method Goodbye.

Delegates and Events

Delegates are ideally suited for use as events — notifications from one component to "listeners" about changes in that component. For more information on the use of delegates for events, see the Events Tutorial.

Delegates vs. Interfaces

Delegates and interfaces are similar in that they enable the separation of specification and implementation. Multiple independent authors can produce implementations that are compatible with an interface specification. Similarly, a delegate specifies the signature of a method, and authors can write methods that are compatible with the delegate specification. When should you use interfaces, and when should you use delegates?

Delegates are useful when:

  • A single method is being called.
  • A class may want to have multiple implementations of the method specification.
  • It is desirable to allow using a static method to implement the specification.
  • An event-like design pattern is desired (for more information, see the Events Tutorial).
  • The caller has no need to know or obtain the object that the method is defined on.
  • The provider of the implementation wants to "hand out" the implementation of the specification to only a few select components.
  • Easy composition is desired.

Interfaces are useful when:

  • The specification defines a set of related methods that will be called.
  • A class typically implements the specification only once.
  • The caller of the interface wants to cast to or from the interface type to obtain other interfaces or classes.

See Also

C# Tutorials


Events Tutorial

This tutorial shows how to declare, invoke, and hook up to events in C#.

Sample Files

See Events Sample to download and build the sample files discussed in this tutorial.

Further Reading

  • event
  • delegate
  • Delegates Tutorial
  • §7.13.3 Event assignment
  • Introduction to Event-Handling Methods

Tutorial

An event in C# is a way for a class to provide notifications to clients of that class when some interesting thing happens to an object. The most familiar use for events is in graphical user interfaces; typically the classes that represent controls in the interface have events that are notified when the user does something to the control (for example, click a button).

Events, however, need not only be used for graphical interfaces. Events provide a generally useful way for objects to signal state changes that may be useful to clients of that object. Events are an important building block for creating classes that can be reused in a large number of different programs.

Events are declared using delegates. If you have not yet studied the Delegates Tutorial, you should do so before continuing. Recall that a delegate object encapsulates a method so that it can be called anonymously. An event is a way for a class to allow clients to give it delegates to methods that should be called when the event occurs. When the event occurs, the delegate(s) given to it by its clients are invoked.

In addition to the examples on declaring, invoking, and hooking up to events, this tutorial also introduces the following topics:

  • Events and Inheritance
  • Events in Interfaces
  • .NET Framework Guidelines

Example 1

The following simple example shows a class, ListWithChangedEvent, which is similar to the standard ArrayList class, but also invokes a Changed event whenever the contents of the list change. Such a general-purpose class could be used in numerous ways in a large program.

For example, a word processor might maintain a list of the open documents. Whenever this list changes, many different objects in the word processor might need to be notified, so that the user interface could be updated. By using events, the code that maintains the list of documents doesn't need to know who needs to be notified — once the list of documents is changed, the event is automatically invoked and every object that needs to be notified is correctly notified. By using events, the modularity of the program is increased.

000: // Events\events1.cs
001: using System;
002: namespace MyCollections
003: {
004:    using System.Collections;
005:
006:    // A delegate type for hooking up change notifications.
007:    public delegate void ChangedEventHandler();
008:
009:    // A class that works just like ArrayList, but sends event
010:    // notifications whenever the list changes.
011:    public class ListWithChangedEvent: ArrayList
012:    {
013:       // An event that clients can use to be notified whenever the
014:       // elements of the list change.
015:       public event ChangedEventHandler Changed;
016:
017:       // Invoke the Changed event; called whenever list changes
018:       protected virtual void OnChanged()
019:       {
020:          if (Changed != null)
021:             Changed();
022:       }
023:
024:       // Override some of the methods that can change the list;
025:       // invoke event after each
026:       public override int Add(object value)
027:       {
028:          int i = base.Add(value);
029:          OnChanged();
030:          return i;
031:       }
032:
033:       public override void Clear()
034:       {
035:          base.Clear();
036:          OnChanged();
037:       }
038:
039:       public override object this[int index]
040:       {
041:          set
042:          {
043:             base[index] = value;
044:             OnChanged();
045:          }
046:       }
047:    }
048: }
049:
050: namespace TestEvents
051: {
052:    using MyCollections;
053:
054:    class EventListener
055:    {
056:       private ListWithChangedEvent List;
057:
058:       public EventListener(ListWithChangedEvent list)
059:       {
060:          List = list;
061:          // Add "ListChanged" to the Changed event on "List".
062:          List.Changed += new ChangedEventHandler(ListChanged);
063:       }
064:
065:       // This will be called whenever the list changes.
066:       private void ListChanged()
067:       {
068:          Console.WriteLine("This is called when the event fires.");
069:       }
070:
071:       public void Detach()
072:       {
073:          // Detach the event and delete the list
074:          List.Changed -= new ChangedEventHandler(ListChanged);
075:          List = null;
076:       }
077:    }
078:
079:    class Test
080:    {
081:       // Test the ListWithChangedEvent class.
082:       public static void Main()
083:       {
084:       // Create a new list.
085:       ListWithChangedEvent list = new ListWithChangedEvent();
086:
087:       // Create a class that listens to the list's change event.
088:       EventListener listener = new EventListener(list);
089:
090:       // Add and remove items from the list.
091:       list.Add("item 1");
092:       list.Clear();
093:       listener.Detach();
094:       }
095:    }
096: }

Output

This is called when the event fires.
This is called when the event fires.

Code Discussion

  • Declaring an event: To declare an event inside a class, first a delegate type for the event must be declared, if none is already declared. The delegate type defines the set of arguments that are passed to the method that handles the event. Multiple events can share the same delegate type, so this step is only necessary if no suitable delegate type has already been declared.
    • Line 007 declares the ChangedEventHandler delegate.

    Next, the event itself is declared. An event is declared like a field of delegate type, except that the keyword event precedes the event declaration, following the modifiers. Events usually are declared public, but any accessibility modifier is allowed.

    • Line 015 declares the Changed event.
  • Invoking an event: Once a class has declared an event, it can treat that event just like a field of the indicated delegate type. The field will either be null, if no client has hooked up a delegate to the event, or else it refers to a delegate that should be called when the event is invoked. Thus, invoking an event is generally done by first checking for null and then calling the event. Invoking an event can only be done from within the class that declared the event.
    • Lines 020 – 021 invoke the Changed event.
  • Hooking up to an event: From outside the class that declared it, an event looks like a field, but access to that field is very restricted. The only things that can be done are:
    • Compose a new delegate onto that field.
    • Remove a delegate from a (possibly composite) field.

    This is done with the += and -= operators. To begin receiving event invocations, client code first creates a delegate of the event type that refers to the method that should be invoked from the event. Then, it composes that delegate onto any other delegates that the event might be connected to, using +=. When the client code is done receiving event invocations, it removes its delegate from the event by using operator -=.

    • Lines 062 and 074 use the += and -= operators to add and detach List.Changed.

Events and Inheritance

When creating a general component that can be derived from, a seeming problem sometimes arises with events. Since events can only be invoked from within the class that declared them, derived classes cannot directly invoke events declared within the base class. Although this is sometimes what is desired, often it is appropriate to give the derived class the freedom to invoke the event. This is typically done by creating a protected invoking method for the event. By calling this invoking method, derived classes can invoke the event. For even more flexibility, the invoking method is often declared as virtual, which allows the derived class to override it. This allows the derived class to intercept the events that the base class is invoking, possibly doing its own processing of them.

In the preceding example, this has been done with the OnChanged method. A derived class could call or override this method if it needed to.

Events in Interfaces

One other difference between events and fields is that an event can be placed in an interface, while a field cannot. When implementing the interface, the implementing class must supply a corresponding event in the class that implements the interface.

.NET Framework Guidelines

Although the C# language allows events to use any delegate type, the .NET Frameworks has some stricter guidelines on the delegate types that should be used for events. If you intend for your component to be used with the .NET Frameworks, you probably will want to follow these guidelines.

The .NET Frameworks guidelines indicate that the delegate type used for an event should take two parameters, an "object source" parameter indicating the source of the event, and an "e" parameter that encapsulates any additional information about the event. The type of the "e" parameter should derive from the EventArgs class. For events that do not use any additional information, the .NET Frameworks has already defined an appropriate delegate type: EventHandler.

Example 2

The following example is a modified version of Example 1 that follows the .NET Framework guidelines. The example uses the EventHandler delegate type.

000: // Events\events2.cs
001: using System;
002: namespace MyCollections
003: {
004:    using System.Collections;
005:
006:    // A class that works just like ArrayList, but sends event
007:    // notifications whenever the list changes.
008:    public class ListWithChangedEvent: ArrayList
009:    {
010:       // An event that clients can use to be notified whenever the
011:       // elements of the list change.
012:       public event EventHandler Changed;
013:
014:       // Invoke the Changed event; called whenever list changes
015:       protected virtual void OnChanged(EventArgs e)
016:       {
017:          if (Changed != null)
018:             Changed(this,e);
019:       }
020:
021:       // Override some of the methods that can change the list;
022:       // invoke event after each
023:       public override int Add(object value)
024:       {
025:          int i = base.Add(value);
026:          OnChanged(EventArgs.Empty);
027:          return i;
028:       }
029:
030:       public override void Clear()
031:       {
032:          base.Clear();
033:          OnChanged(EventArgs.Empty);
034:       }
035:
036:       public override object this[int index]
037:       {
038:          set
039:          {
040:             base[index] = value;
041:             OnChanged(EventArgs.Empty);
042:          }
043:       }
044:    }
045: }
046:
047: namespace TestEvents
048: {
049:    using MyCollections;
050:
051:    class EventListener
052:    {
053:       private ListWithChangedEvent List;
054:
055:       public EventListener(ListWithChangedEvent list)
056:       {
057:          List = list;
058:          // Add "ListChanged" to the Changed event on "List".
059:          List.Changed += new EventHandler(ListChanged);
060:       }
061:
062:       // This will be called whenever the list changes.
063:       private void ListChanged(object sender, EventArgs e)
064:       {
065:          Console.WriteLine("This is called when the event fires.");
066:       }
067:
068:       public void Detach()
069:       {
070:          // Detach the event and delete the list
071:          List.Changed -= new EventHandler(ListChanged);
072:          List = null;
073:       }
074:    }
075:
076:    class Test
077:    {
078:       // Test the ListWithChangedEvent class.
079:       public static void Main()
080:       {
081:       // Create a new list.
082:       ListWithChangedEvent list = new ListWithChangedEvent();
083:
084:       // Create a class that listens to the list's change event.
085:       EventListener listener = new EventListener(list);
086:
087:       // Add and remove items from the list.
088:       list.Add("item 1");
089:       list.Clear();
090:       listener.Detach();
091:       }
092:    }
093: }

Output

This is called when the event fires.
This is called when the event fires.

Code Discussion

In addition to the details discussed in Example 1, the following are important points of Example 2:

  • Line 012 declares the Changed event.
  • Lines 017 – 018 invoke the Changed event. Notice the use of the parameter "e" of the type EventArgs.
  • Lines 059 and 071 use the += and -= operators to add and detach List.Changed.
  • Notice the use of the parameter EventArg.Empy in the override methods (lines 023 – 041).
  • Notice also the use of the "sender" and "e" parameters in the ListChanged method (line 063).

See Also

C# Tutorials


Explicit Interface Implementation Tutorial

This tutorial demonstrates how to explicitly implement interface members and how to access those members from the interface instances.

Sample Files

See Explicit Interface Implementation Sample to download and build the sample files discussed in this tutorial.

Further Reading

  • interface
  • §13.4 Interface implementations
  • §13.4.1 Explicit interface member implementations

Tutorial

A class that implements an interface can explicitly implement a member of that interface. When a member is explicitly implemented, it cannot be accessed through a class instance but only through an instance of the interface. This tutorial contains two examples. The first one illustrates how to explicitly implement and access interface members. The second example shows how to implement two interfaces that have the same member names.

Example 1

This example declares an interface IDimensions, and a class Box, which explicitly implements the interface members Length and Width. The members are accessed through the interface instance myDimensions.

000: // ExplicitInterface\explicit1.cs
001: interface IDimensions
002: {
003:    float Length();
004:    float Width();
005: }
006:
007: class Box : IDimensions
008: {
009:    float lengthInches;
010:    float widthInches;
011:
012:    public Box(float length, float width)
013:    {
014:       lengthInches = length;
015:       widthInches = width;
016:    }
017:    // Explicit interface member implementation:
018:    float IDimensions.Length()
019:    {
020:       return lengthInches;
021:    }
022:    // Explicit interface member implementation:
023:    float IDimensions.Width()
024:    {
025:       return widthInches;
026:    }
027:
028:    public static void Main()
029:    {
030:       Box myBox = new Box(30.0f, 20.0f);
031:       IDimensions myDimensions = (IDimensions) myBox;
032:       //System.Console.WriteLine("Length: {0}", myBox.Length());
033:       //System.Console.WriteLine("Width: {0}", myBox.Width());
034:       System.Console.WriteLine("Length: {0}", myDimensions.Length());
035:       System.Console.WriteLine("Width: {0}", myDimensions.Width());
036:    }
037: }

Output

Length: 30
Width: 20

Code Discussion

  • Lines 018 and 023 declare explicit interface member implementations.
  • Lines 032 and 033 are commented out because they would produce compile errors. An interface member that is explicitly implemented cannot be accessed from a class instance.
  • Lines 034 and 035 successfully print out the dimensions of the box because the methods are being called from an instance of the interface.

Example 2

Explicit interface implementation also allows the programmer to inherit two interfaces that share the same member names and give each interface member a separate implementation. This example displays the dimensions of a box in both metric and English units. The Box class inherits two interfaces IEnglishDimensions and IMetricDimensions, which represent the different measurement systems. Both interfaces have identical member names, Length and Width.

000: // ExplicitInterface\explicit2.cs
001: interface IEnglishDimensions
002: {
003:    float Length();
004:    float Width();
005: }
006: interface IMetricDimensions
007: {
008:    float Length();
009:    float Width();
010: }
011: class Box : IEnglishDimensions, IMetricDimensions
012: {
013:    float lengthInches;
014:    float widthInches;
015:    public Box(float length, float width)
016:    {
017:       lengthInches = length;
018:       widthInches = width;
019:    }
020:    float IEnglishDimensions.Length()
021:    {
022:       return lengthInches;
023:    }
024:    float IEnglishDimensions.Width()
025:    {
026:       return widthInches;
027:    }
028:    float IMetricDimensions.Length()
029:    {
030:       return lengthInches * 2.54f;
031:    }
032:    float IMetricDimensions.Width()
033:    {
034:       return widthInches * 2.54f;
035:    }
036:    public static void Main()
037:    {
038:       Box myBox = new Box(30.0f, 20.0f);
039:       IEnglishDimensions eDimensions = (IEnglishDimensions) myBox;
040:       IMetricDimensions mDimensions = (IMetricDimensions) myBox;
041:       System.Console.WriteLine("Length(in): {0}", eDimensions.Length());
042:       System.Console.WriteLine("Width(in): {0}", eDimensions.Width());
043:       System.Console.WriteLine("Length(cm): {0}", mDimensions.Length());
044:       System.Console.WriteLine("Width(cm): {0}", mDimensions.Width());
045:    }
046: }

Output

Length(in): 30
Width(in): 20
Length(cm): 76.2
Width(cm): 50.8

Code Discussion

  • At line 011, class Box implements two interfaces: IEnglishDimensions and IMetricDimensions.
  • At lines 020 and 024, members of the IEnglishDimensions interface are explicitly implemented.
  • At lines 028 and 032, members of the IMetricDimensions interface are explicitly implemented.
  • If you want to make the default measurements in English units, implement the methods Length and Width normally, and explicitly implement the Length and Width methods from the IMetricDimensions interface:
//normal implementation:
public float Length()
{
   return lengthInches;
}
public float Width()
{
   return widthInches;
}
//explicit implementation:
float IMetricDimensions.Length()
{
   return lengthInches * 2.54f;
}
float IMetricDimensions.Width()
{
   return widthInches * 2.54f;
}
  • In this case, you can access the English units from the class instance and access the metric units from the interface instance:
    System.Console.WriteLine("Length(in): {0}", myBox.Length());
System.Console.WriteLine("Width (in): {0}", myBox.Width());
System.Console.WriteLine("Length(cm): {0}", mDimensions.Length());
System.Console.WriteLine("Width (cm): {0}", mDimensions.Width());

See Also

C# Tutorials


Conditional Methods Tutorial

This tutorial demonstrates conditional methods, which provide a powerful mechanism by which calls to methods can be included or omitted depending on whether a preprocessor symbol is defined.

Sample Files

See Conditional Methods Sample to download and build the sample files discussed in this tutorial.

Further Reading

  • Command-line Arguments
  • §17.4.2 The Conditional attribute
  • Conditional
  • #define
  • /define (Preprocessor Definition)
  • System.Diagnostics Namespace

Tutorial

Conditional methods allow developers to create methods whose calls can be placed in the code and then either included or omitted during compilation based on a preprocessing symbol.

Suppose that you want to enable some assertion code in the debug builds and disable it in the retail builds. In C++, there are two ways to have this functionality in your code:

  • Using #ifdef, define both debug and release versions of a macro. The debug version calls the tracing code, and the release version does nothing. Because C# doesn't support macros, this method doesn't work.
  • Have two implementations of the code being called — in the debug version have full functionality; in the retail version have empty stubs for the methods. Users then choose which one to include when linking the project. The problem with this approach is that the retail builds contain calls to empty methods, and the configuration is more complex.

C# conditional methods provide a simple solution to this problem that is similar to the first approach listed above. There are two basic mechanisms to do this:

  • #define the preprocessing identifier in the source code directly. (See an example of this approach in Conditional.)
  • Define the preprocessing identifier on the C# command line via the /define option (/d). This approach is used in the following example.

Example

This example consists of two source files: the first file is the library that provides a tracing mechanism, and the second file is the client program that uses this library.

File #1: Creating Conditional Methods

The code below shows a simple library that provides a tracing mechanism that displays trace messages to the system console. Clients can embed trace calls in the code and then be able to control whether the tracing gets called by defining symbols in their own compilation phase.

000: // ConditionalMethods\CondMethod.cs
001: using System;
002: using System.Diagnostics;
003: namespace TraceFunctions
004: {
005:     public class Trace
006:     {
007:         [Conditional("DEBUG")]
008:         public static void Message(string traceMessage)
009:         {
010:             Console.WriteLine("[TRACE] - " + traceMessage);
011:         }
012:     }
013: }

Code Discussion

  • Lines 003 – 013 declare a namespace TraceFunctions, which has one class, Trace. The Trace class has a static method Message, which prints out a trace message.
  • Line 007 marks the Message method as being conditional (via the Conditional attribute). The Conditional attribute takes one parameter — the preprocessing identifier that controls whether the method call is included when clients are compiled. If the preprocessing identifier is defined, the method is called; otherwise, the call is never inserted in the client's code.
  • Notice that there are a few restrictions on which methods can be marked as conditional:
    • The Conditional attribute cannot be applied to interface methods.
    • Conditional methods must have a void return type.

File #2: Using a Conditional Method

The following client program uses the Trace class defined in file #1 to do some simple tracing.

000: // ConditionalMethods\tracetest.cs
001: using System;
002: using TraceFunctions;
003:
004: public class TraceClient
005: {
006:     public static void Main(string[] args)
007:     {
008:         Trace.Message("Main Starting");
009:
010:         if (args.Length == 0)
011:         {
012:             Console.WriteLine("No arguments have been passed");
013:         }
014:         else
015:         {
016:             for( int i=0; i < args.Length; i++)
017:             {
018:                 Console.WriteLine("Arg[{0}] is [{1}]",i,args[i]);
019:             }
020:         }
021:
022:         Trace.Message("Main Ending");
023:     }
024: }

Code Discussion

  • Lines 008 and 022 use the Trace.Message method.
  • Conditional code is included in client code depending on whether the preprocessing identifier is defined when the client code gets compiled.

    Compiling with client code with the /d:DEBUG flag means that the compiler inserts the call to the Trace method. If the symbol is not defined, the call is never made.

Sample Run

The command:

tracetest A B C

gives the following output:

[TRACE] - Main Starting
Arg[0] is [A]
Arg[1] is [B]
Arg[2] is [C]
[TRACE] - Main Ending

The command:

tracetest

gives the following output:

[TRACE] - Main Starting
No arguments have been passed
[TRACE] - Main Ending

See Also

C# Tutorials


XML Documentation Tutorial

This tutorial shows how to document code using XML.

Sample Files

See XML Documentation Sample to download and build the sample files discussed in this tutorial.

Further Reading

  • /doc (Process Documentation Comments)
  • Tags for Documentation Comments
  • Processing the XML File

Tutorial

C# provides a mechanism for developers to document their code using XML. In source code files, lines that begin with /// and that precede a user-defined type such as a class, delegate, or interface; member such as a field, event, property, or method; or a namespace declaration can be processed as comments and placed in a file.

Example

The following sample provides a basic overview of a type that has been documented.

000: // XMLDoc\XMLsample.cs
001: using System;
002:
003: /// <summary>
004: /// Class level summary documentation goes here.</summary>
005: /// <remarks>
006: /// Longer comments can be associated with a type or member
007: /// through the remarks tag</remarks>
008: public class SomeClass
009: {
010:    /// <summary>
011:    /// Store for the name property</summary>
012:    private string myName = null;
013:
014:     public SomeClass()
015:     {
016:         //
017:         // TODO: Add Constructor Logic here
018:         //
019:     }
020:
021:    /// <summary>
022:    /// Name property </summary>
023:    /// <value>
024:    /// A value tag is used to describe the property value</value>
025:    public string Name
026:    {
027:       get
028:       {
029:          if ( myName == null )
030:          {
031:             throw new Exception("Name is null");
032:          }
033:
034:          return myName;
035:       }
036:    }
037:
038:    /// <summary>
039:    /// Description for SomeMethod.</summary>
040:    /// <param name="s"> Parameter description for s goes here</param>
041:    /// <seealso cref="String">
042:    /// You can use the cref attribute on any tag to reference a type or member
043:    /// and the compiler will check that the reference exists. </seealso>
044:    public void SomeMethod(string s)
045:    {
046:    }
047:
048:    /// <summary>
049:    /// Some other method. </summary>
050:    /// <returns>
051:    /// Return results are described through the returns tag.</returns>
052:    /// <seealso cref="SomeMethod(string)">
053:    /// Notice the use of the cref attribute to reference a specific method </seealso>
054:    public int SomeOtherMethod()
055:    {
056:       return 0;
057:    }
058:
059:    /// <summary>
060:    /// The entry point for the application.
061:    /// </summary>
062:    /// <param name="args"> A list of command line arguments</param>
063:     public static int Main(String[] args)
064:     {
065:         //
066:         // TODO: Add code to start application here
067:         //
068:
069:         return 0;
070:     }
071: }
072:

Compilation

To compile the sample code, type the following command line:

csc XMLsample.cs /doc:XMLsample.xml

This will create the XML file XMLsample.xml.

Code Discussion

XML documentation starts with ///. The processing of these comments has some restrictions:

  • The documentation must be well-formed XML. If the XML is not well-formed, a warning is generated and the documentation file will contain a comment saying that an error was encountered.
  • Developers are free to create their own set of tags. There is a recommended set of tags (see the Further Reading section). Some of the recommended tags have special meanings:
    • The <param> tag is used to describe parameters. If used, the compiler will verify that the parameter exists and that all parameters are described in the documentation. If the verification failed, the compiler issues a warning.
    • The cref attribute can be attached to any tag to provide a reference to a code element. The compiler will verify that this code element exists. If the verification failed, the compiler issues a warning. The compiler also respects any using statements when looking for a type described in the cref attribute.
    • The <summary> tag is used by IntelliSense inside Visual Studio to display additional information about a type or member.

Output

Here is the resulting XML file from the class above:

<?xml version="1.0"?>
<doc>
    <assembly>
        <name>xmlsample</name>
    </assembly>
    <members>
        <member name="T:SomeClass">
            <summary>
            Class level summary documentation goes here.</summary>
            <remarks>
            Longer comments can be associated with a type or member
            through the remarks tag</remarks>
        </member>
        <member name="F:SomeClass.myName">
            <summary>
            Store for the name property</summary>
        </member>
        <member name="M:SomeClass.SomeMethod(System.String)">
            <summary>
            Description for SomeMethod.</summary>
            <param name="s"> Parameter description for s goes here</param>
            <seealso cref="T:System.String">
            You can use the cref attribute on any tag to reference a type or member
            and the compiler will check that the reference exists. </seealso>
        </member>
        <member name="M:SomeClass.SomeOtherMethod">
            <summary>
            Some other method. </summary>
            <returns>
            Return results are described through the returns tag.</returns>
            <seealso cref="M:SomeClass.SomeMethod(System.String)">
            Notice the use of the cref attribute to reference a specific method </seealso>
        </member>
        <member name="M:SomeClass.Main(System.String[])">
            <summary>
            The entry point for the application.
            </summary>
            <param name="args"> A list of command line arguments</param>
        </member>
        <member name="P:SomeClass.Name">
            <summary>
            Name property </summary>
            <value>
            A value tag is used to describe the property value</value>
        </member>
    </members>
</doc>
Note The XML file does not provide full information about the type and members (for example, it does not contain any type information). To get full information about a type or member, the documentation file must be used in conjunction with reflection on the actual type or member.

See Also

C# Tutorials


Platform Invoke Tutorial

Platform Invocation Services (PInvoke), allows managed code to call unmanaged functions that are implemented in a DLL.

The purpose of the tutorial is to discuss what you need to do to be able to call unmanaged DLL functions from C#. The attributes discussed here tell you how to call these methods and have data types be marshaled correctly.

Sample Files

See Platform Invoke Sample to download and build the sample files discussed in this tutorial.

Further Reading

  • Overview of Platform Invocation Services
  • §19.6 The DllImport attribute
  • §19.14 The MarshalAs attribute
  • §19.20 The StructLayout attribute
  • §19.12 The In and Out attributes
  • Using Attributes
  • DllImportAttribute Class
  • MarshalAsAttribute Class
  • StructLayoutAttribute Class

Tutorial

There are two ways that C# code can directly call unmanaged code:

  • Directly call a native function exported from a DLL.
  • Call an interface method on an unmanaged COM object (for more information, see the COM InterOp Tutorial).

For both methods, you must provide the C# compiler with a declaration of the native method, and you may also need to provide the C# compiler with a description of how to marshal the parameters and return value to and from the native code.

In addition to the examples, the following topics are discussed in this tutorial:

  • Calling a DLL Export Directly from C#
  • Default Marshaling and Specifying Custom Marshaling for Parameters to Native Methods
  • Specifying Custom Marshaling for User-Defined Structs
  • Registering Callback Methods

Calling a DLL Export Directly from C#

To declare a method as having an implementation from a native DLL export, do the following:

  • Declare the method with the static and extern C# keywords.
  • Attach the DllImport attribute to the method. The DllImport attribute allows you to specify the name of the DLL that contains the native method. You can optionally name the method as well (the default behavior is to use the name of the C# method as the exported function).
  • Optionally specify custom marshaling information for the method's parameters and return value, which will override the .NET default marshaling.

Example 1

This example shows you how to use the DllImport attribute to create a message box.

000: // Pinvoke\MessageBox.cs

001: using System;

002: using System.Runtime.InteropServices;

003:

004: class C

005: {

006:     [DllImport("user32.dll")]

007:     public static extern int MessageBoxA(

008:         int h, string m, string c, int type);

009:     public static int Main()

010:     {

011:         return MessageBoxA(0, "Hello World!", "My Message Box", 0);

012:     }

013: }

Output

When you run this example, a message box that contains the "Hello World!" text will pop up on your screen.

Code Discussion

The preceding example shows the minimum requirements for declaring a C# method that is implemented in a native DLL. The method C.MessageBoxA is declared with the static and extern modifiers, and has the DllImport attribute, which tells the compiler that the implementation comes from the user32.dll, using the default name of MessageBoxA.

For more information on the syntax of the DllImport attribute, see the .NET documentation for the DllImportAttribute Class.

Default Marshaling and Specifying Custom Marshaling for Parameters to Native Methods

When calling a native function from C# code, the runtime must marshal the parameters and return values.

For every .NET type, there is a default native type, which .NET will use to marshal data across a native API call. For example, the default marshaling for C# string values is to the native type LPTSTR (pointer to TCHAR char buffer). You can override the default marshaling using the MarshalAs attribute in the C# declaration of the native API.

Example 2

This example shows you how to override the default marshaling by using the MarshalAs attribute.

000: // Pinvoke\Marshal.cs
001: using System;
002: using System.Runtime.InteropServices;
003:
004: class C
005: {
006:     [DllImport("user32.dll")]
007:     public static extern int MessageBoxW(
008:         int h,
009:         [MarshalAs(UnmanagedType.LPWStr)]
010:         string m,
011:         [MarshalAs(UnmanagedType.LPWStr)]
012:         string c,
013:         int type);
014:     public static int Main()
015:     {
016:         return MessageBoxW(0, "Hello World!", "My Message Box", 0);
017:     }
018: }

Output

When you run this example, a message box that contains the "Hello World!" text will pop up on your screen.

Code Discussion

In the preceding example, the default marshaling for the second and third parameters to the MessageBoxW function has been overridden from the default of LPTSTR to LPWSTR.

The MarshalAs attribute can be placed on method parameters, method return values, and fields of structs and classes. To modify the default marshaling of a method return value, place the MarshalAs attribute in the attribute list for the method itself (marshaling of structs is discussed below).

For more information on the syntax of the MarshalAs attribute, see the .NET documentation for the MarshalAsAttribute Class.

Note The In and Out attributes can be used to annotate parameters to native methods. They behave in a similar manner as the in and out modifiers in MIDL source files. Note that the Out attribute is different from the C# parameter modifier, out.

Specifying Custom Marshaling for User-Defined Structs

You can specify custom marshaling attributes for fields of structs and classes passed to/from native APIs. You do this by adding MarshalAs attributes to the fields of the struct or class. You must also use the StructLayout attribute to set the native layout of the struct and optionally to control the default marshaling of string members, and to set the default packing size.

Example 3

This example demonstrates how to specify custom marshaling attributes for a struct.

Consider the following C structure:

typedef struct tagLOGFONT
{
   LONG lfHeight;
   LONG lfWidth;
   LONG lfEscapement;
   LONG lfOrientation;
   LONG lfWeight;
   BYTE lfItalic;
   BYTE lfUnderline;
   BYTE lfStrikeOut;
   BYTE lfCharSet;
   BYTE lfOutPrecision;
   BYTE lfClipPrecision;
   BYTE lfQuality;
   BYTE lfPitchAndFamily;
   TCHAR lfFaceName[LF_FACESIZE];
} LOGFONT;

In C#, you can describe the preceding struct by using the StructLayout and MarshalAs attributes, as follows:

000: // Pinvoke\logfont.cs
001: using System;
002: using System.Runtime.InteropServices;
003:
004: [StructLayout(LayoutKind.Sequential)]
005: public class LOGFONT
006: {
007:     public const int LF_FACESIZE = 32;
008:     public int lfHeight;
009:     public int lfWidth;
010:     public int lfEscapement;
011:     public int lfOrientation;
012:     public int lfWeight;
013:     public byte lfItalic;
014:     public byte lfUnderline;
015:     public byte lfStrikeOut;
016:     public byte lfCharSet;
017:     public byte lfOutPrecision;
018:     public byte lfClipPrecision;
019:     public byte lfQuality;
020:     public byte lfPitchAndFamily;
021:     [MarshalAs(UnmanagedType.ByValTStr, SizeConst=LF_FACESIZE)]
022:     public string lfFaceName;
023: }

For more information on the syntax of the StructLayout attribute, see the .NET documentation for the StructLayoutAttribute Class.

The structure can then be used in C# code as shown below:

000: // Pinvoke\pinvoke.cs
001: using System;
002: using System.Runtime.InteropServices;
003:
004: class C
005: {
006:     [DllImport("gdi32.dll", CharSet=CharSet.Auto)]
007:     public static extern int CreateFontIndirect(
008:         [In, MarshalAs(UnmanagedType.LPStruct)]
009:         LOGFONT lplf   // characteristics
010:     );
011:     public static void Main()
012:     {
013:         LOGFONT lf = new LOGFONT();
014:         lf.lfHeight = 9;
015:         lf.lfFaceName = "Arial";
016:         int i = CreateFontIndirect(lf);
017:         Console.WriteLine(int.Format(i, "X"));
018:     }
019: }

Sample Run

C30A0AE5

Code Discussion

In the preceding example, the CreateFontIndirect method is using a parameter of the type LOGFONT. The MarshalAs and In attributes are used to qualify the parameter. The program displays the numeric value returned by the method as a hexadecimal uppercase string.

Registering Callback Methods

To register a managed callback that calls a native function, declare a delegate with the same argument list and pass an instance of it via PInvoke. On the unmanaged side, it will appear as a function pointer. For more information about PInvoke and callback, see Overview of Platform Invocation Services.

For example, consider the following unmanaged function, MyFunction, which requires callback as one of the arguments:

typedef void (__stdcall *PFN_MYCALLBACK)();
int __stdcall MyFunction(PFN_ MYCALLBACK callback);

To call MyFunction from managed code, declare the delegate, attach DllImport to the function declaration, and optionally marshal any parameters or the return value:

public delegate void MyCallback();
[DllImport("MYDLL.DLL")]
public static extern void MyFunction(MyCallback callback);

Also, make sure the lifetime of the delegate instance covers the time the unmanaged code; otherwise, it gets garbage-collected.

See Also

C# Tutorials


COM InterOp Tutorial

This tutorial demonstrates unmanaged COM InterOp with C#.

Sample Files

See COM InterOp Sample to download and build the sample files discussed in this tutorial.

Further Reading

Attributes

§17. Attributes

Tutorial

C# uses .NET facilities to perform unmanaged COM InterOp. C# currently has support for:

  • creating unmanaged COM objects (CoCreateInstance).
  • determining if an unmanaged COM interface is implemented by an object (QueryInterface).
  • calling methods on unmanaged COM interfaces.
  • implementing objects and interfaces which can be called by unmanaged COM clients.

.NET handles reference-counting issues with COM InterOp so there is no need to call or implement AddRef and Release.

This tutorial covers the following topics:

  • Creating a COM Class Wrapper
  • Declaring an unmanaged COM coclass in C# Code
  • Creating an unmanaged COM Object from C# Code
  • Declaring an unmanaged COM Interface in C#
  • QueryInterface in C#
  • Putting It All Together (examples pulling all the concepts together - using TLBImp and using C# code to use an unmanaged COM object)

Creating an Unmanaged COM Wrapper

For C# code to reference unmanaged COM objects and interfaces, you need to include a .NET definition for the unmanaged COM interfaces in your C# build. The easiest way to do this is to use TLBIMP, a command-line tool included in the .NET SDK. TLBIMP converts an unmanaged COM typelib into .NET metadata — effectively creating a "managed" wrapper that can be called from any managed language. .NET metadata created with TLBIMP can be included in a C# build via the /R compiler option.

TLBIMP performs the following conversions:

  • unmanaged COM coclasses are converted to C# classes with a parameterless constructor and no other methods.
  • unmanaged COM vtable interfaces are converted to C# interfaces.
  • unmanaged COM structs are converted to C# structs with public fields.

A great way to check the output of TLBIMP is to run METAINFO (another .NET SDK tool) to view the result of the conversion.

Although TLBIMP is the preferred method for converting unmanaged COM definitions to C#, it is not always possible (for example there is no typelib for the COM definitions, TLBIMP can't handle the definitions in the typelib). In these cases the alternative is to manually define the unmanaged COM definitions in C# source code using C# attributes. Once you have created the C# source mapping, you simply compile the C# source code to produce the managed wrapper.

The main attributes you need to understand to perform COM mapping are:

  • ComImport - Marks a class as an externally implemented COM class.
  • Guid – Used to specify a universally unique identifier (UUID) for a class or an interface.
  • InterfaceType – specifies whether an interface derives from IUnknown or IDispatch.
  • PreserveSig – specifies whether the native return value should be converted from an HRESULT to an .NET exception. See below for details.

Each of these attributes is shown in the context of an actual example detailed below.

Declaring an unmanaged COM coclass in C# Code

Unmanaged COM coclasses are represented in C# as classes. These classes must have the ComImport attribute associated with them. The following restrictions apply to these classes:

  • The class must be derived from System.Object.
  • The class must implement no interfaces.
  • The class must have no user-defined members.
  • The class must also have a Guid attribute that sets the guid for the class.

An example of declaring a coclass in C# is shown below:

001: //
002: // declare FilgraphManager as an unmanaged COM coclass
003: //
004: [ComImport, Guid("E436EBB3-524F-11CE-9F53-0020AF0BA770")]
005: class FilgraphManager // cannot have a base class or
006:                       // interface list here
007: {
008:     // cannot have any members here
009:     // NOTE that the C# compiler will add a default constructor
010:     // for you (no parameters)
011: }

The C# compiler will add a parameterless constructor that you can call to create an instance of the unmanaged COM coclass.

Creating an Unmanaged COM Object from C# Code

Unmanaged COM coclasses are represented in C# as classes with a parameterless constructor. Creating an instance of this class using the new operator is the COM equivalent of calling CoCreateInstance(). Using the class defined above, it is simple to instantiate the class:

001: class MainClass
002: {
003:     public static void Main()
004:     {
005:         //
006:         // Create an instance of an unmanaged COM coclass - calls
007:         //
008:         // CoCreateInstance(E436EBB3-524F-11CE-9F53-0020AF0BA770,
009:         //                  NULL, CLSCTX_ALL,
010:         //                  IID_IUnknown, &f)
011:         //
012:         // returns null on failure.
013:         //
014:         FilgraphManager f = new FilgraphManager();
015:     }
016: }

Declaring an unmanaged COM interface in C#

Unmanaged COM interfaces are represented in C# as interfaces with ComImport and Guid attributes. They cannot include any interfaces in their base interface list and they must declare the interface member functions in the order that the methods appear in the COM interface.

Unmanaged COM interfaces declared in C# must include declarations for all members of their base interfaces with the exception of members of IUnknown and IDispatch — .NET automatically adds these. Unmanaged COM interfaces which derive from IDispatch must be marked with the InterfaceType attribute.

When calling an unmanaged COM interface method from C# code, the runtime must marshal the parameters and return values to/from the unmanaged COM world. For every .NET type there is a default type that .NET will use to marshal when marshaling across an unmanaged COM call. For example, the default marshaling for C# string values is to the native type LPTSTR (pointer to TCHAR char buffer). You can override the default marshaling using the marshal attribute in the C# declaration of the unmanaged COM interface.

In unmanaged COM, a common way to return success or failure is to return an HRESULT and have an out parameter marked as "retval" in MIDL for the real return value of the method. In C# (and .NET) the standard way to indicate an error has occurred is to throw an exception.

By default .NET provides an automatic mapping between the two styles of exception handling for Unmanaged COM interface methods called by .NET.

  • The return value changes to the signature of the parameter marked retval (void if the method has no parameter marked as retval).
  • The parameter marked as retval is left off of the argument list of the method.

Any non-success return value will cause a System.COMException exception to be thrown.

This example shows an unmanaged COM interface declared in MIDL and the same interface declared in C# (note that the methods use the unmanaged COM error-handling approach):

Here is the original MIDL version of the interface:

 [
  odl,
  uuid(56A868B1-0AD4-11CE-B03A-0020AF0BA770),
  helpstring("IMediaControl interface"),
  dual,
 oleautomation
]
interface IMediaControl : IDispatch
{
  [id(0x60020000)]
  HRESULT Run();

  [id(0x60020001)]
  HRESULT Pause();

  [id(0x60020002)]
  HRESULT Stop();

  [id(0x60020003)]
  HRESULT GetState( [in] long msTimeout, [out] long* pfs);

  [id(0x60020004)]
  HRESULT RenderFile([in] BSTR strFilename);

  [id(0x60020005)]
  HRESULT AddSourceFilter( [in] BSTR strFilename, [out] IDispatch** ppUnk);

  [id(0x60020006), propget]
  HRESULT FilterCollection([out, retval] IDispatch** ppUnk);

  [id(0x60020007), propget]
  HRESULT RegFilterCollection([out, retval] IDispatch** ppUnk);

  [id(0x60020008)]
  HRESULT StopWhenReady();
};

Here is the C# equivalent of this interface:

001: using System.Runtime.InteropServices;
002:
003: // declare IMediaControl as an unmanaged COM interface which
004: // derives from Interface
005: [Guid("56A868B1-0AD4-11CE-B03A-0020AF0BA770"),
006:     InterfaceType(ComInterfaceType.InterfaceIsDual)]
007: interface IMediaControl // can not list any base interfaces here
008: {
009:     // Note that the members of IUnknown and Interface are NOT
010:     // listed here
011:     //
012:     void Run();
013:
014:     void Pause();
015:
016:     void Stop();
017:
018:     void GetState( [In] int msTimeout, [Out] out int pfs);
019:
020:     void RenderFile(
021:     [In, MarshalAs(UnmanagedType.BStr)] string strFilename);
022:
023:     void AddSourceFilter(
024:     [In, MarshalAs(UnmanagedType.BStr)] string strFilename,
025:     [Out, MarshalAs(UnmanagedType.Interface)] out object ppUnk);
026:
027:     [MarshalAs(UnmanagedType.Interface)]
028:     object FilterCollection();
029:
030:     [MarshalAs(UnmanagedType.Interface)]
031:     object RegFilterCollection();
032:
033:     void StopWhenReady();
034: }

Note how the C# interface has mapped the error-handling cases. If the unmanaged COM method returns an error, an exception will be raised on the C# side.

To prevent the translation of HRESULTs to COMExceptions, attach the PreserveSig(true) attribute to the method in the C# declaration. See the .NET documentation for the System.Runtime.InteropServices.PreserveSigAttribute for more details.

QueryInterface in C#

C# coclasses are not very useful until you can access an interface which that object implements. In C++ you would navigate an object's interfaces using the QueryInterface() method on IUnknown. In C# you can do the same thing by explicitly casting the unmanaged COM object to the desired unmanaged COM interface. If the QueryInterface fails, then an invalid cast exception is thrown.

Example:

001:     //
002:     // create an instance of an unmanaged COM coclass
003:     //
004: FilgraphManager graphManager = new FilgraphManager();
005:
006:     //
007:     // Attempt to QueryInterface() the unmanaged COM object to see
008:     // if it supports the IMediaControl unmanaged COM interface.
009:     // Note that this will throw a System.InvalidCastException if
010:     // the QueryInterface fails
010:     //
012: IMediaControl mc = (IMediaControl) graphManager;
013:
014:     //
015:     // Now you call a method on an unmanaged COM interface
016:     //
017: mc.Run();

Putting It All Together

Here is a complete example that shows an AVI file viewer written in C#. The program creates an instance of a COM coclass, casts it to an unmanaged COM interface, then calls methods on the unmanaged COM interface.

There are two possible approaches:

  • Use TLBIMP to create the .NET class for you.
  • Write C# code that manually does the unmanaged COM mapping.

The suggested approach is that the first point of call be TLBIMP. Run TLBIMP on the unmanaged COM TLB and use METAINFO to look at the result. If this is successful, then simply use the .NET DLL created and continue with your program from there. If TLBIMP does not work for you (or if you have no TLB), then you can manually create the C# wrapper as detailed above.

The example demonstrates both approaches, even through the TLBIMP tool was successful.

Using TLBIMP

Initial steps:

  • Run TLBIMP over the TLB. The Media Player used in this example is contained in Quartz.DLL, which should be in your Windows system directory. Use the following command to create the .NET DLL:

    tlbimp c:\winnt\system32\quartz.dll /out:QuartzTypeLib.dll

    Note that the resulting DLL needs to be named QuartzTypeLib (this is so .NET can load the containing types correctly when running — make sure that this DLL is on the CORPATH. CORPATH is an environment variable that .NET uses to look for classes).

  • Use the MetaInfo tool to display the results.
  • Build the program using the C# compiler option /R to include the QuartzTypeLib.dll.

Once you have the QuartzTypeLib DLL, you can use the program below to use it to render a movie (a example movie to use can be found in your Windows directory (clock.avi).

000: // COMInterOP\interop1.cs
001: using System;
002: class MainClass
003: {
004: /********************************************************
005: Abstract: This method collects the filename of an AVI to show
006: then creates an instance of the Quartz unmanaged COM object.
007: To show the AVI, the program calls RenderFile and Run on
008: IMediaControl. Quartz uses its own thread and window to display
009: the AVI.The main thread blocks on a ReadLine until the user hits
010: enter.
011:     Input Parameters: None
012:     Returns: int: Standard 32 bit process return code.
013: *********************************************************/
014:     public static int Main(string[] args)
015:     {
016:             // Check to see if the user passed in a filename
017:         if (args.Length != 1)
018:         {
019:             DisplayUsage();
020:             return 0;
021:         }
022:
023:         string filename = args[0];
024:         if (filename.Equals("/?"))
025:         {
026:             DisplayUsage();
027:             return 0;
028:         }
029:
030: // Create instance of Quartz
031: // calls CoCreateInstance(E436EBB3-524F-11CE-9F53-
032: //   0020AF0BA770, NULL, CLSCTX_ALL, IID_IUnknown, &graphManager)
033: // returns null on failure.
034:
035:         QuartzTypeLib.FilgraphManager graphManager =
036:             new QuartzTypeLib.FilgraphManager();
037:
038: // QueryInterface for the IMediaControl interface
039:
040:         QuartzTypeLib.IMediaControl mc =
041:             (QuartzTypeLib.IMediaControl)graphManager;
042:
043: // call some methods on an unmanaged COM interface
044: //
045: // Pass in file to RenderFile method on unmanaged COM object.
046:         mc.RenderFile(filename);
047:
048: // Show file.
049:         mc.Run();
050:
051: // Wait for completion.
052:         Console.WriteLine("Hit Enter to Continue.");
053:         Console.ReadLine();
054:         return 0;
055:     }
056:
057:     private static void DisplayUsage()
058:     {
059: // User did not provide enough parameters.
060: // Display usage.
061:         Console.WriteLine("VideoPlayer: Plays AVI files.");
062:         Console.WriteLine("Usage: VIDEOPLAYER.EXE filename");
063:         Console.WriteLine("where filename is the full path and");
064:         Console.WriteLine("file name of the AVI to display.");
065:     }
066: }

C# Code Approach

This example uses the same "Main" method as above, but instead of running TLBIMP, it simply maps the Media Player unmanaged COM object using C#.

000: // COMInterOp\interop2.cs
001: using System;
002: using System.Runtime.InteropServices;
003:
004: namespace QuartzTypeLib
005: {
006: // declare IMediaControl as an unmanaged COM interface which
007: // derives from Interface
008:     [Guid("56A868B1-0AD4-11CE-B03A-0020AF0BA770"),
009:         InterfaceType(ComInterfaceType.InterfaceIsDual)]
010:     interface IMediaControl // can not list any base
011:                                 // interfaces here
012:     {
013: // Note that IUnknown Interface members are NOT listed here
014:
015:         [PreserveSig(false)]
016:         void Run();
017:
018:         [PreserveSig(false)]
019:         void Pause();
020:
021:         [PreserveSig(false)]
022:         void Stop();
023:
024:         [PreserveSig(false)]
025:         void GetState( [In] int msTimeout, [Out] out int pfs);
026:
027:         [PreserveSig(false)]
028:         void RenderFile(
029:             [In, MarshalAs(UnmanagedType.BStr)] string strFilename);
030:
031:         [PreserveSig(false)]
032:         void AddSourceFilter(
033:             [In, MarshalAs(UnmanagedType.BStr)] string strFilename,
034:             [Out, MarshalAs(UnmanagedType.Interface)]
035:                 out object ppUnk);
036:
037:         [PreserveSig(false), MarshalAs(UnmanagedType.Interface)]
038:         object FilterCollection();
039:
040:         [PreserveSig(false), MarshalAs(UnmanagedType.Interface)]
041:         object RegFilterCollection();
042:
043:         [PreserveSig(false)]
044:         void StopWhenReady();
045:     }
046:         //
047:         // declare FilgraphManager as an unmanaged COM coclass
048:         //
049:     [ComImport, Guid("E436EBB3-524F-11CE-9F53-0020AF0BA770")]
050:     class FilgraphManager // cannot have a base class or
051:                          // interface list here
052:     {
053:        // cannot have any members here
054:        // NOTE that the C# compiler will add a default constructor
055:        // for you (no parameters)
056:     }
057: }
058:
059: class MainClass
060: {
061:     /******************************************************
062:     Function : main
063:     Abstract: This method collects the filename of an AVI to show
064:     then creates an instance of the Quartz unmanaged COM object.
065:     To show the AVI, the program calls RenderFile and Run on
066:     IMediaControl. Quartz uses its own thread and window to
067:     display the AVI. The main thread blocks on a ReadLine until
068:     the user hits enter.
069:         Input Parameters: None
070:         Returns: int: Standard 32 bit process return code.
071:     *******************************************************/
072:
073:     public static int Main(string[] args)
074:     {
075:             // Check to see if the user passed in a filename
076:         if (args.Length != 1)
077:         {
078:             DisplayUsage();
079:             return 0;
080:         }
081:
082:         String filename = args[0];
083:         if (filename.Equals("/?"))
084:         {
085:             DisplayUsage();
086:             return 0;
087:         }
088:         //
089:         // Create instance of Quartz
090:         // calls CoCreateInstance(E436EBB3-524F-11CE-9F53-
091:         // 0020AF0BA770, NULL, CLSCTX_ALL, IID_IUnknown,
092:         // &graphManager)
093:         // returns null on failure. Shouldn't this throw on
094:         // failure?
095:         //
096:         QuartzTypeLib.FilgraphManager graphManager =
097:             new QuartzTypeLib.FilgraphManager();
098:         //
099:         // QueryInterface for the IMediaControl interface
100:         //
101:         QuartzTypeLib.IMediaControl mc =
102:             (QuartzTypeLib.IMediaControl)graphManager;
103:         //
104:         // call some methods on an unmanaged COM interface
105:         //
106:         // Pass in file to RenderFile method on unmanaged COM
107:         // object.
108:         mc.RenderFile(filename);
109:
110:         // Show file.
111:         mc.Run();
112:
113:         // Wait for completion.
114:         Console.WriteLine("Hit Enter to Continue.");
115:         Console.ReadLine();
116:         return 0;
117:     }
118:
119:     private static void DisplayUsage()
120:     {
121:         // User did not provide enough parameters.
122:         // Display usage.
123:         Console.WriteLine("VideoPlayer: Plays AVI files.");
124:         Console.WriteLine("Usage: VIDEOPLAYER.EXE filename");
125:         Console.WriteLine("where filename is the full path and");
126:         Console.WriteLine("file name of the AVI to display.");
127:     }
128: }

See Also

C# Tutorials


Attributes Tutorial

This tutorial shows how to create custom attribute classes, use them in code, and query them through reflection.

Sample Files

See Attributes Sample to download and build the sample files discussed in this tutorial.

Further Reading

  • C# Attributes
  • §17. Attributes
  • AttributeUsage
  • Discovering Type Information at Runtime

Tutorial

Attributes provide a powerful method of associating declarative information with C# code (types, methods, properties, and so forth). Once associated with a program entity, the attribute can be queried at run time and used in any number of ways.

Example usage of attributes include:

  • Associating help documentation with program entities (through a Help attribute).
  • Associating value editors to a specific type in a GUI framework (through a ValueEditor attribute).

In addition to a complete example, this tutorial includes the following topics:

  • Declaring an Attribute Class The first thing you need to be able to do is declare an attribute.
  • Using an Attribute Class Once the attribute has been created, you then associate the attribute with a particular program element.
  • Accessing Attributes Via Reflection Once the attribute has been associated with a program element, you use reflection to query its existence and its value.

Declaring an Attribute Class

Declaring an attribute in C# is simple — it takes the form of a class declaration that inherits from System.Attribute and has been marked with the AttributeUsage attribute as shown below:

using System;
[AttributeUsage(AttributeTargets.All)]
public class HelpAttribute : System.Attribute
{
    public readonly string Url;

    public string Topic               // Topic is a named parameter
    {
       get
       {
          return topic;
       }
       set
       {
          topic = value;
       }
    }

    public HelpAttribute(string url)   // url is a positional
    {                                  // parameter
        this.Url = url;
    }

    private string topic;
}

Code Discussion

  • The attribute AttributeUsage specifies the language elements to which the attribute can be applied.
  • Attributes classes are public classes derived from System.Attribute that have at least one public constructor.
  • Attribute classes have two types of parameters:
    • Positional parameters must be specified every time the attribute is used. Positional parameters are specified as constructor arguments to the attribute class. In the example above, url is a positional parameter.
    • Named parameters are optional. If they are specified when the attribute is used, the name of the parameter must be used. Named parameters are defined by having a nonstatic field or property. In the example above, Topic is a named parameter.
  • Attribute parameters are restricted to constant values of the following types:
    • Simple types (bool, byte, char, short, int, long, float, and double).
    • string.
    • System.Type.
    • enums.
    • object (The argument to an attribute parameter of type object must be a constant value of one of the above types.)
    • One-dimensional arrays of any of the above types.

Parameters for the AttributeUsage Attribute

The attribute AttributeUsage provides the underlying mechanism by which attributes are declared.

AttributeUsage has one positional parameter:

  • AllowOn Specifies the program elements that the attribute can be assigned to (class, method, property, parameter, and so on). Valid values for this parameter can be found in the System.Attributes.AttributeTargets enumeration in the .NET Frameworks. The default value for this parameter is all program elements (AttributeElements.All).

AttributeUsage has one named parameter:

  • AllowMultiple A Boolean value that indicates whether multiple attributes can be specified for one program element. The default value for this parameter is false.

Using an Attribute Class

Here's a simple example of using the attribute declared in the previous section:

[HelpAttribute("http://msvc/MyClassInfo")]
class MyClass
{
}

In this example, the HelpAttribute attribute is associated with MyClass.

Accessing Attributes Through Reflection

Once attributes have been associated with program elements, reflection can be used to query their existence and values. The main reflection methods to query attributes are contained in the System.Reflection.MemberInfo class (GetCustomAttributes family of methods). The following example demonstrates the basic way of using reflection to get access to attributes:

class MainClass
{
    public static void Main()
    {
        System.Reflection.MemberInfo info = typeof(MyClass);
        object[] attributes = info.GetCustomAttributes();
        for (int i = 0; i < attributes.Length; i ++)
        {
            System.Console.WriteLine(attributes[i]);
        }
    }
}

Example

The following is a complete example where all pieces are brought together.

000: // Attributes\AttributesTutorial.cs
001: // This example shows the use of class and method attributes.
002:
003: using System;
004: using System.Reflection;
005: using System.Collections;
006:
007: //
008: // The IsTested class is a user-defined custom attribute class
009: //
010: // It can be applied to any declaration including
011: //  - types (struct, class, enum, delegate)
012: //  - members (methods, fields, events, properties, indexers)
013: //
014: // It is used with no arguments
015: //
016: public class IsTestedAttribute : Attribute
017: {
018:     public override string ToString()
019:     {
020:         return "Is Tested";
021:     }
022: }
023:
024: //
025: // The AuthorAttribute class is a user-defined attribute class
026: //
027: // It can be applied to classes and struct declarations only.
028: //
029: // It takes one unnamed string argument (the author's name)
030: // It has one optional named argument Version which is of type int
031: //
032: [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
033: public class AuthorAttribute : Attribute
034: {
035:     //
036:     // This constructor specifies the unnamed arguments to the attribute class
037:     //
038:     public AuthorAttribute(string name)
039:     {
040:         this.name = name;
041:         this.version = 0;
042:     }
043:
044:     //
045:     // This property is readonly (it has no set accessor)
046:     // so it cannot be used as a named argument to this attribute
047:     //
048:     public string Name
049:     {
050:         get
051:         {
052:             return name;
053:         }
054:     }
055:
056:     //
057:     // This property is read-write (it has a set accessor)
058:     // so it can be used as a named argument when using this
059:     // class as an attribute class
060:     //
061:     public int Version
062:     {
063:         get
064:         {
065:             return version;
066:         }
067:         set
068:         {
069:             version = value;
070:         }
071:     }
072:
073:     public override string ToString()
074:     {
075:         string value = "Author : " + Name;
076:         if (version != 0)
077:         {
078:             value += " Version : " + Version.ToString();
079:         }
080:         return value;
081:     }
082:
083:     private string name;
084:     private int version;
085: }
086:
087: //
088: // Here you attach the AuthorAttribute user-defined custom attribute to
089: // the Account class. The unnamed string argument is passed to the
090: // AuthorAttribute class's constructor when creating the attributes.
091: //
092: [Author("Joe Programmer")]
093: class Account
094: {
095:     //
096:     // Attach the IsTestedAttribute custom attribute to this method
097:     //
098:     [IsTested]
099:     public void AddOrder(Order orderToAdd)
100:     {
101:         orders.Add(orderToAdd);
102:     }
103:
104:     private ObjectList orders = new ObjectList();
105: }
106:
107: //
108: // Attach the AuthorAttribute and IsTestedAttribute custom attributes
109: // to this class.
110: //
111: // Note the use of the 'Version' named argument to the AuthorAttribute
112: //
113: [Author("Jane Programmer", Version = 2), IsTested()]
114: class Order
115: {
116:     // add stuff here ...
117: }
118:
119: class MainClass
120: {
121:    private static bool IsMemberTested(MemberInfo member)
122:    {
123:         foreach (object attribute in member.GetCustomAttributes())
124:         {
125:             if (attribute is IsTestedAttribute)
126:             {
127:                return true;
128:             }
129:         }
130:       return false;
131:    }
132:
133:     private static void DumpAttributes(MemberInfo member)
134:     {
135:         Console.WriteLine("Attributes for : " + member.Name);
136:         foreach (object attribute in member.GetCustomAttributes())
137:         {
138:             Console.WriteLine(attribute);
139:         }
140:     }
141:
142:     public static void Main()
143:     {
144:         //
145:         // display attributes for Account class
146:         //
147:         DumpAttributes(typeof(Account));
148:
149:         //
150:         // display list of tested members
151:         //
152:         foreach (MethodInfo method in (typeof(Account)).GetMethods())
153:         {
154:             if (IsMemberTested(method))
155:             {
156:                Console.WriteLine("Member {0} is tested!", method.Name);
157:             }
158:             else
159:             {
160:                Console.WriteLine("Member {0} is NOT tested!", method.Name);
161:             }
162:         }
163:         Console.WriteLine();
164:
165:         //
166:         // display attributes for Order class
167:         //
168:         DumpAttributes(typeof(Order));
169:
170:         //
171:         // display attributes for methods on the Order class
172:         //
173:         foreach (MethodInfo method in (typeof(Order)).GetMethods())
174:         {
175:            if (IsMemberTested(method))
176:            {
177:                Console.WriteLine("Member {0} is tested!", method.Name);
178:            }
179:            else
180:            {
181:                Console.WriteLine("Member {0} is NOT tested!", method.Name);
182:            }
183:         }
184:         Console.WriteLine();
185:     }
186: }

Output

Attributes for : Account
Author : Joe Programmer
Member GetHashCode is NOT tested!
Member Equals is NOT tested!
Member ToString is NOT tested!
Member AddOrder is tested!
Member GetType is NOT tested!

Attributes for : Order
Author : Jane Programmer Version : 2
Is Tested
Member GetHashCode is NOT tested!
Member Equals is NOT tested!
Member ToString is NOT tested!
Member GetType is NOT tested!

See Also

C# Tutorials


Security Tutorial

This tutorial discusses .NET security and shows two ways to modify security permissions in C#: permission classes and permission attributes.

Sample Files

See Security Sample to download and build the sample files discussed in this tutorial.

Further Reading

Tutorial

Most application and component developers should not need to do anything special in order to work with the .NET security system and benefit from the safety protection it provides.

The only code that represents an exception and requires more in-depth knowledge and special consideration of the security system is secure libraries. These are the code that represents the boundary between the secure managed world and unmanaged native code (that is outside the ability of the .NET security infrastructure to enforce). These libraries typically must be highly trusted to work, and are the one place in managed code where a programming error can potentially expose a security vulnerability. Code access security can't eliminate the potential for human error, but compared to the much larger volume of application code that uses a few secure libraries, the amount of code that requires close scrutiny is dramatically reduced.

Security

.NET security protects your code and data from being misused or damaged by other code by enforcing security restrictions on managed code. .NET applications request the permissions they need and security policy established by an administrator chooses to grant permissions based on determination of trust, or if the request is too much refuses to run the code. Trust is based on evidence about the code such as a digital signature, where the code comes from, and so forth. Once granted, security enforces permissions that control what code is (and by not being granted, what code is not) allowed to do.

Permissions

.NET security allows code to use protected resources only if it has "permission" to do so. To express this, .NET uses the concept of permissions, which represent the right for code to access protected resources. Code requests the permissions it needs, and the security policy applied by .NET determines which permissions the code is actually granted.

.NET provides code access permission classes, each of which encapsulates the ability to access a particular resource. You use these permissions to indicate to .NET what your code needs to be allowed to do and to indicate what your code's callers must be authorized to do. Policy also uses these objects to determine what permissions to grant to code.

Policy

Enforcement of security policy is what makes .NET managed code safe. Every assembly that loads is subject to security policy that grants code permissions based on trust, with trust based on evidence about the code. See the .NET documentation for information on administering security policy.

Modifying Security Permissions in C#

There are two ways to modify the current security permissions in C#: using calls to permission classes in the .NET frameworks, or using security permission attributes.

Here's an example of using .NET frameworks calls to remove the UnmanagedCode permission:

000: // Security\SecurityClasses.cs
001: using System;
002: using System.Security;
003: using System.Security.Permissions;
004: using System.Runtime.InteropServices;
005:
006: class NativeMethods
007: {
008:     //
009:     // this is a call to unmanaged code. Executing this method requires
010:     // the UnmanagedCode security permission. Without this permission
011:     // an attempt to call this method will throw a SecurityException
012:     //
013:     [DllImport("user32.dll")]
014:     public static extern int MessageBox(uint hWnd, string lpText, string lpCaption, uint uType);
015: }
016:
017: class MainClass
018: {
019:     private static void CallUnmanagedCodeWithoutPermission()
020:     {
021:         //
022:         // create a security permission object to describe the UnmanagedCode permission
023:         //
024:         SecurityPermission perm = new SecurityPermission(SecurityPermissionFlag.UnmanagedCode);
025:
026:         //
027:         // remove the UnmanagedCode from our current set of permissions
028:         //
029:         perm.Deny();
030:
031:         try
032:         {
033:             Console.WriteLine("Attempting to call unmanaged code without permission.");
034:             NativeMethods.MessageBox(0, "Hello World!", "", 0);
035:             Console.WriteLine("Called unmanaged code without permission. Whoops!");
036:         }
037:         catch (SecurityException)
038:         {
039:             Console.WriteLine("Caught Security Exception attempting to call unmanaged code.");
040:         }
041:     }
042:
043:     private static void CallUnmanagedCodeWithPermission()
044:     {
045:         //
046:         // create a security permission object to describe the UnmanagedCode permission
047:         //
048:         SecurityPermission perm = new SecurityPermission(SecurityPermissionFlag.UnmanagedCode);
049:
050:         //
051:         // add the UnmanagedCode to our current set of permissions
052:         //
053:         perm.Assert();
054:
055:         try
056:         {
057:             Console.WriteLine("Attempting to call unmanaged code with permission.");
058:             NativeMethods.MessageBox(0, "Hello World!", "", 0);
059:             Console.WriteLine("Called unmanaged code with permission.");
060:         }
061:         catch (SecurityException)
062:         {
063:             Console.WriteLine("Caught Security Exception attempting to call unmanaged code. Whoops!");
064:         }
065:     }
066:
067:     public static void Main()
068:     {
069:         CallUnmanagedCodeWithoutPermission();
070:         CallUnmanagedCodeWithPermission();
071:     }
072: }

Here is the same example using attributes for the security permissions:

000: // Security\SecurityAttributes.cs
001: using System;
002: using System.Security;
003: using System.Security.Permissions;
004: using System.Runtime.InteropServices;
005:
006: class NativeMethods
007: {
008:     //
009:     // this is a call to unmanaged code. Executing this method requires
010:     // the UnmanagedCode security permission. Without this permission
011:     // an attempt to call this method will throw a SecurityException
012:     //
013:     [DllImport("user32.dll")]
014:     public static extern int MessageBox(uint hWnd, string lpText, string lpCaption, uint uType);
015: }
016:
017: class MainClass
018: {
019:     //
020:     // the security permission attached to this method will remove the UnmanagedCode
021:     // permission from the current set of permissions for the duration of the call to this method
022:     //
023:     [SecurityPermission(SecurityAction.Deny, Flags = SecurityPermissionFlag.UnmanagedCode)]
024:     private static void CallUnmanagedCodeWithoutPermission()
025:     {
026:         try
027:         {
028:             Console.WriteLine("Attempting to call unmanaged code without permission.");
029:             NativeMethods.MessageBox(0, "Hello World!", "", 0);
030:             Console.WriteLine("Called unmanaged code without permission. Whoops!");
031:         }
032:         catch (SecurityException)
033:         {
034:             Console.WriteLine("Caught Security Exception attempting to call unmanaged code.");
035:         }
036:     }
037:
038:     //
039:     // the security permission attached to this method will add the UnmanagedCode permission
040:     // to the current set of permissions for the duration of the call to this method
041:     //
042:     [SecurityPermission(SecurityAction.Assert, Flags = SecurityPermissionFlag.UnmanagedCode)]
043:     private static void CallUnmanagedCodeWithPermission()
044:     {
045:         try
046:         {
047:             Console.WriteLine("Attempting to call unmanaged code with permission.");
048:             NativeMethods.MessageBox(0, "Hello World!", "", 0);
049:             Console.WriteLine("Called unmanaged code with permission.");
050:         }
051:         catch (SecurityException)
052:         {
053:             Console.WriteLine("Caught Security Exception attempting to call unmanaged code. Whoops!");
054:         }
055:     }
056:
057:     public static void Main()
058:     {
059:         CallUnmanagedCodeWithoutPermission();
060:         CallUnmanagedCodeWithPermission();
061:     }
062: }

Security and Performance

The .NET security system prevents malicious code downloaded from the network from damaging your computer system. However, these security checks are not without cost, even if your code never throws a SecurityException. You can disable the UnmanagedCode security demand when making calls to unmanaged code by using the SuppressUnmanagedCodeSecurity attribute (line 016 below).

000: // Security\SuppressSecurity.cs
001: using System;
002: using System.Security;
003: using System.Security.Permissions;
004: using System.Runtime.InteropServices;
005:
006: class NativeMethods
007: {
008:     //
009:     // this is a call to unmanaged code. Executing this method requires
010:     // the UnmanagedCode security permission. Without this permission
011:     // an attempt to call this method will throw a SecurityException
012:     //
013:     // NOTE: The SuppressUnmanagedCodeSecurity attribute disables the check for
014:     // NOTE: the UnmanagedCode permission. Be Careful!
015:     //
016:     [SuppressUnmanagedCodeSecurity()]
017:     [DllImport("user32.dll")]
018:     internal static extern int MessageBox(uint hWnd, string lpText, string lpCaption, uint uType);
019: }
020:
021: class MainClass
022: {
023:     //
024:     // the security permission attached to this method will remove the UnmanagedCode
025:     // permission from the current set of permissions for the duration of the call to this method
026:     //
027:     [SecurityPermission(SecurityAction.Deny, Flags = SecurityPermissionFlag.UnmanagedCode)]
028:     private static void CallUnmanagedCodeWithoutPermission()
029:     {
030:         try
031:         {
032:             //
033:             // We've disabled the UnmanagedCode security check on the call below.
034:             // However, the unmanaged call only displays UI. We'll ensure security
035:             // by only allowing the unmanaged call if we have UI permission.
036:             //
037:             UIPermission uiPermission = new UIPermission(PermissionState.Unrestricted);
038:             uiPermission.Demand();
039:
040:             Console.WriteLine("Attempting to call unmanaged code without UnmanagedCode permission.");
041:             NativeMethods.MessageBox(0, "Hello World!", "", 0);
042:             Console.WriteLine("Called unmanaged code without UnmanagedCode permission.");
043:         }
044:         catch (SecurityException)
045:         {
046:             Console.WriteLine("Caught Security Exception attempting to call unmanaged code.");
047:         }
048:     }
049:
050:     //
051:     // the security permission attached to this method will add the UnmanagedCode permission
052:     // to the current set of permissions for the duration of the call to this method
053:     //
054:     [SecurityPermission(SecurityAction.Assert, Flags = SecurityPermissionFlag.UnmanagedCode)]
055:     private static void CallUnmanagedCodeWithPermission()
056:     {
057:         try
058:         {
059:             Console.WriteLine("Attempting to call unmanaged code with permission.");
060:             NativeMethods.MessageBox(0, "Hello World!", "", 0);
061:             Console.WriteLine("Called unmanaged code with permission.");
062:         }
063:         catch (SecurityException)
064:         {
065:             Console.WriteLine("Caught Security Exception attempting to call unmanaged code. Whoops!");
066:         }
067:     }
068:
069:     public static void Main()
070:     {
071:         CallUnmanagedCodeWithoutPermission();
072:         CallUnmanagedCodeWithPermission();
073:     }
074: }

Note that the above example allows both unmanaged calls to succeed even though the first call doesn't have UnmanagedCode permission. When using the SuppressUnmanagedCodeSecurityAttribute, you should use other security checks to ensure that you don't incur a security hole. In the above example, this is done by adding the Demand for the UIPermission (line 038) before the unmanaged call, which ensures that the caller has permission to display UI.

See Also

C# Tutorials


Threading Tutorial

See Also

C# Tutorials


Unsafe Code Tutorial

This tutorial demonstrates how to use unmanaged code (code using pointers) in C#.

Sample Files

See Unsafe Code Sample to download and build the sample files discussed in this tutorial.

Further Reading

  • §19.1 Unsafe code
  • unsafe
  • fixed

Tutorial

C# allows you to write unmanaged code — code using pointers — in cases where performance is critical or in other situations where pointers are required.

Note that unmanaged code is not verifiable by the Common Language Runtime and can only be executed when trusted.

Example

The following example uses pointers to copy an array of bytes from src to dst.

000: // Unsafe\fastcopy.cs
001: using System;
002:
003: class Test
004: {
005:     static unsafe void Copy(byte[] src, int srcIndex,
006:                             byte[] dst, int dstIndex, int count)
007:     {
008:         if (src == null || srcIndex < 0 ||
009:             dst == null || dstIndex < 0 || count < 0)
010:         {
011:             throw new ArgumentException();
012:         }
013:         int srcLen = src.Length;
014:         int dstLen = dst.Length;
015:         if (srcLen - srcIndex < count ||
016:             dstLen - dstIndex < count)
017:         {
018:             throw new ArgumentException();
019:         }
020:         fixed (byte* pSrc = src, pDst = dst)
021:         {
022:             byte* ps = pSrc;
023:             byte* pd = pDst;
024:             for (int n = count >> 2; n != 0; n--)
025:             {
026:                 *((int*)pd) = *((int*)ps);
027:                 pd += 4;
028:                 ps += 4;
029:             }
030:             for (count &= 3; count != 0; count--)
031:             {
032:                 *pd = *ps;
033:                 pd++;
034:                 ps++;
035:             }
036:         }
037:     }
038:
039:     static void Main(string[] args)
040:     {
041:         byte[] a = new byte[100];
042:         byte[] b = new byte[100];
043:         for(int i=0; i<100; ++i)
044:            a[i] = (byte)i;
045:         Copy(a, 0, b, 0, 100);
046:         Console.WriteLine("The first 10 elements are:");
047:         for(int i=0; i<10; ++i)
048:            Console.Write(b[i]+" ");
049:     }
050: }

Output

The first 10 elements are:
0 1 2 3 4 5 6 7 8 9

Code Discussion

  • Line 005 contains the unsafe keyword, which allows pointers to be used within the function.
  • Line 020 uses the fixed statement to declare pointers to the source and destination arrays. The fixed statement "pins" the location of the src and dst objects in memory so that they will not be moved by garbage collection. The objects will be unpinned when the fixed block completes
  • Line 024 loops over the count in blocks of 4 bytes, copying an integer at a time. Remember that int in C# is strictly a 4-byte type.
  • Line 030 completes the copy by moving any bytes that weren't moved in blocks of 4.

Compile with the /unsafe option.

See Also

C# Tutorials


ADO Tutorial

ADO (ActiveX Data Objects) is an application-programming interface for accessing data in a connected (cursor-based scenario). ADO provides consistent high-performance access to data and supports a variety of development needs, including the creation of front-end database clients and middle-tier business objects using live connections to data in relational databases and other stores. ADO's primary benefits are ease of use, high speed, low memory overhead, and a small disk footprint.

ADO provides an easy-to-use interface to OLE DB, which provides the underlying access to the data.

Sample Files

See ADO Sample to download and build the sample files discussed in this tutorial.

Further Reading

  • System.Data.ADO Namespace

Tutorial

This tutorial demonstrates how to use ADO objects from C#. It shows how you can create a dataset and add tables to it from a database. The BugTypes.mdb database used in the sample program is a Microsoft Access 2000 .MDB file.

Example

This program accesses the BugTypes.mdb database, creates a dataset, adds the tables to it, and displays the number of tables, columns, and rows. It also displays the content of the each row.

000: // ADO\adosample.cs
001: using System;
002: using System.Data;
003: using System.Data.ADO;
004:
005: public class MainClass
006: {
007:    public static void Main ()
008:    {
009:       // set Access connection and select strings
010:       string strAccessConn = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=BugTypes.MDB";
011:       string strAccessSelect = "SELECT * FROM Categories";
012:
013:       //Create the dataset and add the Categories table to it
014:       DataSet myDataSet = new DataSet();
015:       myDataSet.Tables.Add("Categories");
016:
017:       // create my Access objects
018:       ADOConnection myAccessConn = new ADOConnection(strAccessConn);
019:       ADODataSetCommand myAccessDataSetCmd = new ADODataSetCommand();
020:       myAccessDataSetCmd.SelectCommand = new ADOCommand(strAccessSelect,myAccessConn);
021:
022:       myAccessConn.Open();
023:       try
024:       {
025:          myAccessDataSetCmd.FillDataSet(myDataSet,"Categories");
026:       }
027:       finally
028:       {
029:          myAccessConn.Close();
030:       }
031:
032:       try
033:       {
034:          // A dataSet can contain multiple tables, so let's get them all into an array
035:          DataTable[] dta = myDataSet.Tables.All;
036:          foreach (DataTable dt in dta)
037:          {
038:             Console.WriteLine("Found data table {0}", dt.TableName);
039:          }
040:
041:          //The next two lines show two different ways you can get the count of tables in a dataset
042:          Console.WriteLine("{0} tables in data set", myDataSet.Tables.Count);
043:          Console.WriteLine("{0} tables in data set", dta.Length);
044:          //The next several lines show how to get information on a specific table by name from the dataset
045:          Console.WriteLine("{0} rows in Categories table", myDataSet.Tables["Categories"].Rows.Count);
046:          //The column info is automatically fetched from the database, so we can read it here
047:          Console.WriteLine("{0} columns in Categories table", myDataSet.Tables["Categories"].Columns.Count);
048:          DataColumn[] drc = myDataSet.Tables["Categories"].Columns.All;
049:          int i = 0;
050:          foreach (DataColumn dc in drc)
051:          {
052:             //Print the column subscript, then the column's name and its data type
053:             Console.WriteLine("Column name[{0}] is {1}, of type {2}",i++ , dc.ColumnName, dc.DataType);
054:          }
055:          DataRow[] dra = myDataSet.Tables["Categories"].Rows.All;
056:          foreach (DataRow dr in dra)
057:          {
058:             //Print the CategoryID as a subscript, then the CategoryName
059:             Console.WriteLine("CategoryName[{0}] is {1}", dr[0], dr[1]);
060:          }
061:       }
062:       catch (Exception e)
063:       {
064:          Console.WriteLine("Oooops.  Caught an exception:\n{0}", e.Message);
065:       }
066:    }
067: }

Output

The Categories table of the BugTypes.mdb database contains the following information:

Category ID | Category Name
1 | Bugbash stuff
2 | Appweek Bugs
3 | .NET Reports
4 | Internal support

When you run the sample the following output will be displayed on the screen:

Found data table Categories
1 tables in data set
1 tables in data set
4 rows in Categories table
2 columns in Categories table
Column name[0] is CategoryID, of type Int32
Column name[1] is CategoryName, of type System.String
CategoryName[1] is Bugbash stuff
CategoryName[2] is Appweek Bugs
CategoryName[3] is .NET Reports
CategoryName[4] is Internal support

Code Discussion

  • Lines 011 and 012 set the connection to the database and the query string.
  • Lines 015 and 016 create the dataset and add the Categories table to it.
  • Lines 019 – 021 create the access objects.
  • In line 023, the access connection is opened. In line 030, the connection is closed.
  • Lines 043 and 044 show different ways to get and display the number of tables in a dataset.
  • Line 046 – 048 show how to get and display information on a specific table from a dataset.
  • Lines 049 – 055 read the column information and display the column subscript, name, and data type.
  • Lines 056 – 061 read the data from the table and display the Category ID and the Category Name.

See Also

C# Tutorials


C# Keywords

Keywords are predefined reserved identifiers that have special meanings to the compiler. They cannot be used as identifiers in your program unless they include @ as a prefix. For example, @if is a legal identifier but if is not because it is a keyword.

abstract | enum | long | stackalloc
as | event | namespace | static
base | explicit | new | string
bool | extern | null | struct
break | false | object | switch
byte | finally | operator | this
case | fixed | out | throw
catch | float | override | true
char | for | params | try
checked | foreach | private | typeof
class | goto | protected | uint
const | if | public | ulong
continue | implicit | readonly | unchecked
decimal | in | ref | unsafe
default | int | return | ushort
delegate | interface | sbyte | using
do | internal | sealed | virtual
double | is | short | void
else | lock | sizeof | while

See Also

C# Preprocessor Directives | C# Operators | C# Programmer's Reference


Types

The C# typing system contains the following categories:

  • Value types
  • Reference types
  • Pointer types

Variables of the value types store data, while those of the reference types store references to the actual data. Reference types are also referred to as objects. Pointer types can be used only in unsafe.

It is possible to convert a value type to a reference type and vice versa, by using boxing and unboxing.

This section also introduces the void type.

See Also

C# Keywords | Types Reference Tables


Value Types

The value types consist of two main categories:

  • Struct type
  • Enumeration type

The struct types contain the user-defined struct types and the following built-in simple types:

  • Numeric types
    • Integral types
    • Floating-point types
    • decimal
  • bool

Remarks

A variable of a value type always contains a value of that type. The assignment to a variable of a value type creates a copy of the assigned value, while the assignment to a variable of a reference type creates a copy of the reference but not of the referenced object.

All value types are derived implicitly from the Object class.

Unlike reference types, it is not possible to derive a new type from a value type. However, like reference types, structs can implement interfaces.

Unlike reference types, it is not possible for a value type to contain the null value.

Each value type has an implicit default constructor that initializes the default value of that type. For information on default values of value types, see Default Values Table.

Following are the main features of simple types:

  • All the simple types are aliases of the .NET System types. For example, int is an alias of System.Int32. For a complete list of aliases, see Built-in Types Table.
  • Constant expressions, whose operands are all simple type constants, are evaluated at compilation time. For more information, see §7.15 Constant expressions.
  • Simple types can be initialized using literals. For example 'A' is a literal of the type char and 2000 is a literal of the type int.

See Also

C# Keywords | Types | Types Reference Tables | Reference Types


bool

The bool keyword is an alias of System.Boolean. It is used to declare variables to store the Boolean values, true and false.

Literals

You can assign a Boolean value to a bool variable, for example:

bool MyVar = true;

You can also assign an expression that evaluates to bool to a bool variable, for example:

bool Alphabetic = (c > 64 && c < 123);

Conversions

In C++, a value of type bool can be converted to a value of type int; in other words, false is equivalent to zero and true is equivalent to nonzero values. In C#, there is no conversion between the bool type and other types. For example, the following if statement is illegal in C#, while it is legal in C++:

int x = 123;
if (x)   // Illegal in C#
{
   printf("The value of x is nonzero.");
}

To test a variable of the type int, you have to explicitly compare it to a value (for example, zero), that is:

int x = 123;
if (x != 0)   // The C# way
{
   Console.Write("The value of x is nonzero.");
}

Example

In this example, you enter a character from the keyboard and the program checks if the input character is letter. If so, it checks if it is lowercase or uppercase. In each case, the proper message is displayed.

// Character Tester
using System;
public class BoolTest
{
   public static void Main()
   {
      Console.Write("Enter a character: ");
      char c = (char) Console.Read();

      if (Char.IsLetter(c))
         if (Char.IsLower(c))
            Console.WriteLine("The character is lowercase.");
         else
            Console.WriteLine("The character is uppercase.");
      else
         Console.WriteLine("The character is not an alphabetic character.");
   }
}

Sample Runs

Enter a character: X
The character is uppercase.

Enter a character: x
The character is lowercase.

Enter a character: 2
The character is not an alphabetic character.

See Also

C# Keywords | Default Values Table | Built-in Types Table | Operator Overloading Tutorial


byte

The byte keyword denotes an integral type that stores values as indicated in the following table.

Type | Range | Size | .NET Type
byte | 0 to 255 | Unsigned 8-bit integer | System.Byte

Literals

You can declare and initialize a byte variable like this example:

byte myByte = 255;

In the preceding declaration, the integer literal 255 is implicitly converted from int to byte. If the integer literal exceeds the range of byte, a compilation error will occur.

Conversions

There is a predefined implicit conversion from byte to short, ushort, int, uint, long, ulong, float, double, or decimal.

You cannot implicitly convert nonliteral numeric types of larger storage size to byte (see Integral Types Table for the storage sizes of integral types). Consider, for example, the following two byte variables x and y:

byte x = 10, y = 20;

The following assignment statement will produce a compilation error, because the arithmetic expression on the right-hand side of the assignment operator evaluates to int by default.

byte z = x + y;   // Error: conversion from int to byte

To fix this problem, use a cast:

byte z = (byte)(x + y);   // OK: explicit conversion

It is possible though to use the following statements, where the destination variable has the same storage size or a larger storage size:

int x = 10, y = 20;
int m = x + y;
long n = x + y;

Also, there is no implicit conversion from floating-point types to byte. For example, the following statement generates a compiler error unless an explicit cast is used:

byte x = 3.0;         // Error: no implicit conversion from double
byte y = (byte)3.0;   // OK: explicit conversion

When calling overloaded methods, a cast must be used. Consider, for example, the following overloaded methods that use byte and int parameters:

public static void MyMethod(int i) {}
public static void MyMethod(byte b) {}

Using the byte cast guarantees that the correct type is called, for example:

MyMethod(5);        // Calling the method with the int parameter
MyMethod((byte)5);  // Calling the method with the byte parameter

For information on arithmetic expressions with mixed floating-point types and integral types, see float and double.

For more information on implicit numeric conversion rules, see the Implicit Numeric Conversions Table.

See Also

C# Keywords | Integral Types Table | Built-in Types Table | Implicit Numeric Conversions Table | Explicit Numeric Conversions Table


char

The char keyword is used to declare a Unicode character in the range indicated in the following table. Unicode characters are 16-bit characters used to represent most of the known written languages throughout the world.

Type | Range | Size | .NET Type
char | U+0000 to U+ffff | Unicode 16-bit character | System.Char

Literals

Constants of the char type can be written as character literals, hexadecimal escape sequence, or Unicode representation. You can also cast the integral character codes. All of the following statements declare a char variable and initialize it with the character X:

char MyChar = 'X';        // Character literal
char MyChar = '\x0058';   // Hexadecimal
char MyChar = (char)88;   // Cast from integral type
char MyChar = '\u0058';   // Unicode

Conversions

A char can be implicitly converted to ushort, int, uint, long, ulong, float, double, or decimal. However, there are no implicit conversions from other types to the char type.

See Also

C# Keywords | Integral Types Table | Built-in Types Table | Implicit Numeric Conversions Table | Explicit Numeric Conversions Table


decimal

The decimal keyword denotes a 128-bit data type. Compared to floating-point types, the decimal type has a greater precision and a smaller range, which makes it suitable for financial and monetary calculations. The approximate range and precision for the decimal type are shown in the following table.

Type | Approximate Range | Precision | .NET Type
decimal | 1.0 × 10-28 to 7.9 × 1028 | 28-29 significant digits | System.Decimal

Literals

If you want a numeric real literal to be treated as decimal, use the suffix m or M, for example:

decimal myMoney = 300.5m;

Without the suffix m, the number is treated as a double, thus generating a compiler error.

Conversions

The integral types are implicitly converted to decimal and the result evaluates to decimal. Therefore you can initialize a decimal variable using an integer literal, without the suffix, for example:

decimal myMoney = 300;

There is no implicit conversion between floating-point types and the decimal type; therefore, a cast must be used to convert between these two types. For example:

decimal myMoney = 99.9m;
double x = (double) myMoney;
myMoney = (decimal) x;

You can also mix decimal and numeric integral types in the same expression. However, mixing decimal and floating-point types without a cast results in a compilation error.

For more information on implicit numeric conversions, see Implicit Numeric Conversions Table.

For more information on explicit numeric conversions, see Explicit Numeric Conversions Table.

Example

In this example, a decimal and an int are mixed in the same expression. The result evaluates to the decimal type.

// decimal conversion
using System;
public class TestDecimal {
   public static void Main ()
   {
      decimal d = 9.1m;
      double x = 2.9;
      int y = 3;
      Console.WriteLine(d + y);   // Result converted to decimal
   }
}

Output

12.1

In the preceding example, if you attempt to add the double and decimal variables by using a statement like this:

Console.WriteLine(d + x);   // error

you will get the following error:

Operator '+' cannot be applied to operands of type 'double' and 'decimal'

Formatting Decimal Output

You can format the results using the Format method of the runtime library of the .NET Framework, as shown in the following example.

Example

In this example, the Format method is used to display the results. Notice that x is rounded because the decimal places exceed $0.99. y, which represents the maximum exact digits, is displayed exactly in the proper format.

// Decimal type formatting
using System;
public class TestDecimalFormat
{
   public static void Main ()
   {
      decimal x = 0.999m;
      decimal y = 9999999999999999999999999999m;
      Console.Write("My amount = ");
      Console.WriteLine(decimal.Format(x, "C"));
      Console.Write("Your amount = ");
      Console.WriteLine(decimal.Format(y, "C"));
   }
}

Output

My amount = $1.00
Your amount = $9,999,999,999,999,999,999,999,999,999.00

For more information on the Format method, see the Format Class. For more information on the decimal type precision, see §4.1.6 The decimal type.

See Also

C# Keywords | Default Values Table | Built-in Types Table | Floating-Point Types Table | Implicit Numeric Conversions Table | Explicit Numeric Conversions Table


double

The double keyword denotes a simple type that stores 64-bit floating-point values. The following table shows the precision and approximate range for the double type.

Type | Approximate range | Precision | .NET Type
double | ±5.0 × 10-324 to ±1.7 × 10308 | 15-16 digits | System.Double

Literals

By default, a real numeric literal on the right-hand side of the assignment operator is treated as double. However, if you want an integer number to be treated as double, use the suffix d or D, for example:

double x = 3D;

Conversions

You can mix numeric integral types and floating-point types in an expression. In this case, the integral types are converted to floating-point types. The evaluation of the expression is performed according to the following rules:

  • If one of the floating-point types is double, the expression evaluates to double (or bool in the case of relational or Boolean expressions).
  • If there is no double type in the expression, it evaluates to float (or bool in the case of relational or Boolean expressions).

A floating-point expression can contain the following sets of values:

  • Positive and negative zero
  • Positive and negative infinity
  • Not-a-Number value (NaN)
  • The finite set of nonzero values

For more information on these values, refer to IEEE Standard for Binary Floating-Point Arithmetic, available on the Web site http://www.ieee.org/.

For more information on floating-point value sets, see §4.1.5 Floating point types.

Example

In the following example, an int, a short, a float, and a double are added together giving a double result.

// Mixing types in expressions
using System;
class MixedTypes
{
   public static void Main()
   {
      int x = 3;
      float y = 4.5f;
      short z = 5;
      double w = 1.7E+3;
      Console.WriteLine("The sum is {0}", x + y + z + w);   // double result
   }
}

Output

The sum is 1712.5

See Also

C# Keywords | Default Values Table | Built-in Types Table | Floating-Point Types Table | Implicit Numeric Conversions Table | Explicit Numeric Conversions Table


enum

The enum keyword is used to declare an enumeration, a distinct type consisting of a set of named constants called the enumerator list. Every enumeration type has an underlying type, which can be any integral type except char. The declaration of an enum variable takes the form:

[attributes] [modifiers] enum identifier [:base-type] {enumerator-list};

where:

attributes (Optional)
Additional declarative information. For more information on attributes and attribute classes, see §17. Attributes.
modifiers (Optional)
The allowed modifiers are new and the four access modifiers.
identifier
The enum name.
base-type (Optional)
One of the integral types except char. The default is int.
enumerator-list
The enumerators' identifiers separated by commas, optionally including a value assignment.

Remarks

The default type of the enumeration elements is int. By default, the first enumerator has the value 0, and the value of each successive enumerator is increased by one. For example:

enum Days {Sat, Sun, Mon, Tue, Wed, Thu, Fri};

In this enumeration, Sat is 0, Sun is 1, Mon is 2, and so forth. Enumerators can have initializers to override the default values. For example:

enum Days {Sat=1, Sun, Mon, Tue, Wed, Thu, Fri};

In this enumeration the sequence of elements is forced to start from 1 instead of 0.

The default value of a enum E is the value produced by the expression (E)0.

For more information on enumeration types, see §14. Enums.

Example

// enum initialization:
using System;
public class EnumTest
{
   enum Days {Sat=1, Sun, Mon, Tue, Wed, Thu, Fri};

   public static void Main()
   {
      int x = (int) Days.Sun;
      int y = (int) Days.Fri;
      Console.WriteLine("Sun = {0}", x);
      Console.WriteLine("Fri = {0}", y);
   }
}

Output

Sun = 2
Fri = 7

However, if you remove the initializer from Sat=1, the result will be:

Sun = 1
Fri = 6

Example

In this example, the base-type option is used to declare an enum whose members are of the type long.

// Using long enumerators
using System;
public class EnumTest
{
   enum Range :long {Max = 2147483648L, Min = 255L};
   public static void Main()
   {
      long x = (long) Range.Max;
      long y = (long) Range.Min;
      Console.WriteLine("Max = {0}", x);
      Console.WriteLine("Min = {0}", y);
   }
}

Output

Max = 2147483648
Min = 255

See Also

C# Keywords | Default Values Table | Built-in Types Table | Types Categories | Value Types


float

The float keyword denotes a simple type that stores 32-bit floating-point values. The following table shows the precision and approximate range for the float type.

Type | Approximate range | Precision | .NET Type
float | ±1.5 × 10-45 to ±3.4 × 1038 | 7 digits | System.Single

Literals

By default, a real numeric literal on the right-hand side of the assignment operator is treated as double. Therefore, to initialize a float variable use the suffix f or F, for example:

float x = 3.5F;

If you don't use the suffix in the previous declaration, you will get a compilation error because you are attempting to store a double value into a float variable.

Conversions

You can mix numeric integral types and floating-point types in an expression. In this case, the integral types are converted to floating-point types. The evaluation of the expression is performed according to the following rules:

  • If one of the floating-point types is double, the expression evaluates to double (or bool in the case of relational or Boolean expressions).
  • If there is no double type in the expression, the expression evaluates to float (or bool in the case of relational or Boolean expressions).

A floating-point expression can contain the following sets of values:

  • Positive and negative zero
  • Positive and negative infinity
  • Not-a-Number value (NaN)
  • The finite set of nonzero values

For more information on these values, refer to IEEE Standard for Binary Floating-Point Arithmetic, available on the Web site http://www.ieee.org/.

For more information on floating-point value sets, see §4.1.5 Floating point types.

Example

In the following example, an int, a short, and a float are included in a mathematical expression giving a float result (notice that there is no double in the expression).

// Mixing types in expressions
using System;
class MixedTypes
{
   public static void Main()
   {
      int x = 3;
      float y = 4.5f;
      short z = 5;
      Console.WriteLine("The result is {0}", x*y/z);
   }
}

Output

The result is 2.70000005

See Also

C# Keywords | Default Values Table | Built-in Types Table | Floating-Point Types Table | Implicit Numeric Conversions Table | Explicit Numeric Conversions Table


int

The int keyword denotes an integral type that stores values according to the size and range shown in the following table.

Type | Range | Size | .NET Type
int | -2,147,483,648 to 2,147,483,647 | Signed 32-bit integer | System.Int32

Literals

You can declare and initialize a variable of the type int like this example:

int myInt = 123;

When an integer literal has no suffix, its type is the first of these types in which its value can be represented: int, uint, long, ulong. In this example, it is of the type int.

Conversions

There is a predefined implicit conversion from int to long, float, double, or decimal. For example:

float myFloat = 123;   // OK: implicit conversion to float

There is a predefined implicit conversion from sbyte, byte, short, ushort, or char to int. For example, if you have a long variable, myLong, the following assignment statement will produce a compilation error without a cast:

long myLong = 22;
int myInt = myLong;       // Error: no implicit conversion from long
int myInt = (int)myLong;  // OK: explicit conversion

Notice also that there is no implicit conversion from floating-point types to int. For example, the following statement generates a compiler error unless an explicit cast is used:

int x = 3.0;         // Error: no implicit conversion from double
int y = (int)3.0;    // OK: explicit conversion

For information on arithmetic expressions with mixed floating-point types and integral types, see float and double.

See Also

C# Keywords | Integral Types Table | Built-in Types Table | Implicit Numeric Conversions Table | Explicit Numeric Conversions Table


long

The long keyword denotes an integral type that stores values according to the size and range shown in the following table.

Type | Range | Size | .NET Type
long | –9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 | Signed 64-bit integer | System.Int64

Literals

You can declare and initialize a long variable like this example:

long myLong = 4294967296;

When an integer literal has no suffix, its type is the first of these types in which its value can be represented: int, uint, long, ulong. In the preceding example, it is of the type long because it exceeds the range of uint (see Integral Types Table for the storage sizes of integral types).

You can also use the suffix L or l with the long type like this:

long myLong = 4294967296L;

When you use the suffix L or l, the type of the literal integer is determined to be either long or ulong according to its size. In the case it is long because it less than the range of ulong.

A common use of the suffix is with calling overloaded methods. Consider, for example, the following overloaded methods that use long and int parameters:

public static void MyMethod(int i) {}
public static void MyMethod(long l) {}

Using the suffix L or l guarantees that the correct type is called, for example:

MyMethod(5);    // Calling the method with the int parameter
MyMethod(5L);   // Calling the method with the long parameter

You can use the long type with other numeric integral types in the same expression, in which case the expression is evaluated as long (or bool in the case of relational or Boolean expressions). For example, the following expression evaluates as long:

898L + 88

For information on arithmetic expressions with mixed floating-point types and integral types, see float and double.

Conversions

There is a predefined implicit conversion from long to float, double, or decimal. Otherwise a cast must be used. For example, the following statement will produce a compilation error without an explicit cast:

int x = 8L;        // Error: no implicit conversion from long to int
int x = (int)8L;   // OK: explicit conversion to int

There is a predefined implicit conversion from sbyte, byte, short, ushort, int, uint, or char to long.

Notice also that there is no implicit conversion from floating-point types to long. For example, the following statement generates a compiler error unless an explicit cast is used:

long x = 3.0;         // Error: no implicit conversion from double
long y = (long)3.0;   // OK: explicit conversion

See Also

C# Keywords | Integral Types Table | Built-in Types Table | Implicit Numeric Conversions Table | Explicit Numeric Conversions Table


sbyte

The sbyte keyword denotes an integral type that stores values according to the size and range shown in the following table.

Type | Range | Size | .NET Type
sbyte | -128 to 127 | Signed 8-bit integer | System.SByte

Literals

You can declare and initialize an sbyte variable like this example:

sbyte mySbyte = 127;

In the preceding declaration, the integer literal 127 is implicitly converted from int to sbyte. If the integer literal exceeds the range of sbyte, a compilation error will occur.

A cast must be used when calling overloaded methods. Consider, for example, the following overloaded methods that use sbyte and int parameters:

public static void MyMethod(int i) {}
public static void MyMethod(sbyte b) {}

Using the sbyte cast guarantees that the correct type is called, for example:

MyMethod(5);         // Calling the method with the int parameter
MyMethod((sbyte)5);  // Calling the method with the sbyte parameter

Conversions

There is a predefined implicit conversion from sbyte to short, int, long, float, double, or decimal.

You cannot implicitly convert nonliteral numeric types of larger storage size to sbyte (see Integral Types Table for the storage sizes of integral types). Consider, for example, the following two sbyte variables x and y:

sbyte x = 10, y = 20;

The following assignment statement will produce a compilation error, because the arithmetic expression on the right-hand side of the assignment operator evaluates to int by default.

sbyte z = x + y;   // Error: conversion from int to sbyte

To fix this problem, use a cast:

sbyte z = (byte)(x + y);   // OK: explicit conversion

It is possible though to use the following statements, where the destination variable has the same storage size or a larger storage size:

sbyte x = 10, y = 20;
int m = x + y;
long n = x + y;

Notice also that there is no implicit conversion from floating-point types to sbyte. For example, the following statement generates a compiler error unless an explicit cast is used:

sbyte x = 3.0;         // Error: no implicit conversion from double
sbyte y = (sbyte)3.0;  // OK: explicit conversion

For information on arithmetic expressions with mixed floating-point types and integral types, see float and double.

For more information on implicit numeric conversion rules, see the Implicit Numeric Conversions Table.

See Also

C# Keywords | Integral Types Table | Built-in Types Table | Implicit Numeric Conversions Table | Explicit Numeric Conversions Table


short

The short keyword denotes an integral data type that stores values according to the size and range shown in the following table.

Type | Range | Size | .NET Type
short | -32,768 to 32,767 | Signed 16-bit integer | System.Int16

Literals

You can declare and initialize a short variable like this example:

short x = 32767;

In the preceding declaration, the integer literal 32767 is implicitly converted from int to short. If the integer literal does not fit into a short storage location, a compilation error will occur.

A cast must be used when calling overloaded methods. Consider, for example, the following overloaded methods that use short and int parameters:

public static void MyMethod(int i) {}
public static void MyMethod(short s) {}

Using the short cast guarantees that the correct type is called, for example:

MyMethod(5);         // Calling the method with the int parameter
MyMethod((short)5);  // Calling the method with the short parameter

Conversions

There is a predefined implicit conversion from short to int, long, float, double, or decimal.

You cannot implicitly convert nonliteral numeric types of larger storage size to short (see Integral Types Table for the storage sizes of integral types). Consider, for example, the following two short variables x and y:

short x = 5, y = 12;

The following assignment statement will produce a compilation error, because the arithmetic expression on the right-hand side of the assignment operator evaluates to int by default.

short z = x + y;   // Error: no conversion from int to short

To fix this problem, use a cast:

short z = (short)(x + y);   // OK: explicit conversion

It is possible though to use the following statements, where the destination variable has the same storage size or a larger storage size:

int m = x + y;
long n = x + y;

There is no implicit conversion from floating-point types to short. For example, the following statement generates a compiler error unless an explicit cast is used:

short x = 3.0;          // Error: no implicit conversion from double
short y = (short)3.0;   // OK: explicit conversion

For information on arithmetic expressions with mixed floating-point types and integral types, see float and double.

For more information on implicit numeric conversion rules, see the Implicit Numeric Conversions Table.

See Also

C# Keywords | Integral Types Table | Built-in Types Table | Implicit Numeric Conversions Table | Explicit Numeric Conversions Table


struct

A struct type is a value type that can contain constructors, constants, fields, methods, properties, indexers, operators, and nested types. The declaration of a struct takes the following form:

[attributes] [modifiers] struct identifier [:interfaces] body [;]
where:
attributes (Optional)
Additional declarative information. For more information on attributes and attribute classes, see §17. Attributes.
modifiers (Optional)
The allowed modifiers are new and the four access modifiers.
identifier
The struct name.
interfaces (Optional)
Interfaces implemented by the struct.
body
The struct body that contains member declarations.

Remarks

The struct type is suitable for representing lightweight objects such as Point, Rectangle, and Color. Although it is possible to represent a point as a class, a struct is more efficient in some scenarios. For example, if you declare an array of 1000 Point objects, you will allocate additional memory for referencing each object. In this case, the struct is less expensive.

It is an error to declare a default (parameterless) constructor for a struct.

It is an error to initialize an instance field in a struct.

Unlike C++, you cannot declare a class using the keyword struct. In C#, classes and structs are semantically different. A struct is a value type, while a class is a reference type. For more information on features of value types, see Value Types.

Unless you need reference type semantics, a class that is smaller than 16 bytes may be more efficiently handled by the system as a struct.

For more information on struct, see §11. Structs.

Example

This example demonstrates struct initialization using both default and parameterized constructors.

// struct declaration and initialization
using System;
public struct Point
{
   public int x, y;

   public Point(int p1, int p2)
   {
      x = p1;
      y = p2;
   }
}

class MainClass
{
   public static void Main()
   {
      // Initialize:
      Point myPoint = new Point();
      Point yourPoint = new Point(10,10);

      // Display results:
      Console.Write("My Point:\t");
      Console.WriteLine("x = {0}, y = {1}", myPoint.x, myPoint.y);
      Console.Write("Your Point:\t");
      Console.WriteLine("x = {0}, y = {1}", yourPoint.x, yourPoint.y);
   }
}

Output

My Point:       x = 0, y = 0
Your Point:     x = 10, y = 10

Example

This example demonstrates a feature that is unique to structs. It creates a Point object without using the new operator. If you replace the word struct with the word class, the program won't compile.

// Declare a struct object without "new"
using System;
public struct Point
{
   public int x, y;

   public Point(int x, int y)
   {
      this.x = x;
      this.y = y;
   }
}

class MainClass
{
   public static void Main()
   {
      // Declare an object:
      Point myPoint;

      // Initialize:
      myPoint.x = 10;
      myPoint.y = 20;

      // Display results:
      Console.WriteLine("My Point:");
      Console.WriteLine("x = {0}, y = {1}", myPoint.x, myPoint.y);
   }
}

Output

My Point:
x = 10, y = 20

See Also

C# Keywords | Default Values Table | Built-in Types Table | Types Categories | Structs Tutorial | Value Types


uint

The uint keyword denotes an integral type that stores values according to the size and range shown in the following table.

Type | Range | Size | .NET Type
uint | 0 to 4,294,967,295 | Unsigned 32-bit integer | System.UInt32

Literals

You can declare and initialize a variable of the type uint like this example:

uint myUint = 4294967290;

When an integer literal has no suffix, its type is the first of these types in which its value can be represented: int, uint, long, ulong. In this example, it is uint.

You can also use the suffix u or U, like this:

uint myUint = 123U;

When you use the suffix U or u, the literal type is determined to be either uint or ulong according to its size. In this example, it is uint.

Conversions

There is a predefined implicit conversion from uint to long, ulong, float, double, or decimal. For example:

float myFloat = 4294967290;   // OK: implicit conversion to float

There is a predefined implicit conversion from byte, ushort, or char to uint. Otherwise you must use a cast. For example, if you have a long variable, myLong, the following assignment statement will produce a compilation error without a cast:

long myLong = 22;
uint myUint = myLong;        // Error: no implicit conversion from long
uint myUint = (uint)myLong;  // OK: explicit conversion

Notice also that there is no implicit conversion from floating-point types to uint. For example, the following statement generates a compiler error unless an explicit cast is used:

uint x = 3.0;         // Error: no implicit conversion from double
uint y = (uint)3.0;   // OK: explicit conversion

For information on arithmetic expressions with mixed floating-point types and integral types, see float and double.

For more information on implicit numeric conversion rules, see the Implicit Numeric Conversions Table.

See Also

C# Keywords | Integral Types Table | Built-in Types Table | Implicit Numeric Conversions Table | Explicit Numeric Conversions Table


ulong

The ulong keyword denotes an integral type that stores values according to the size and range shown in the following table.

Type | Range | Size | .NET Type
ulong | 0 to 18,446,744,073,709,551,615 | Unsigned 64-bit integer | System.UInt64

Literals

You can declare and initialize a ulong variable like this example:

ulong myUlong = 9223372036854775808;

When an integer literal has no suffix, its type is the first of these types in which its value can be represented: int, uint, long, ulong. In this example, it is of the type ulong.

You can also use suffixes to specify the type of the literal according to the following rules:

  • If you use L or l, the type of the literal integer will be either long or ulong according to its size.
  • If you use U or u, the type of the literal integer will be either uint or ulong according to its size.
  • If you use UL, ul, Ul, uL, LU, lu, Lu, or lU, the type of the literal integer will be ulong.

    For example, the output of the following three statements will be the system type UInt64, which corresponds to the alias ulong:

    Console.WriteLine(9223372036854775808L.GetType());
Console.WriteLine(123UL.GetType());
Console.WriteLine((123UL + 456).GetType());

A common use of the suffix is with calling overloaded methods. Consider, for example, the following overloaded methods that use ulong and int parameters:

public static void MyMethod(int i) {}
public static void MyMethod(ulong l) {}

Using a suffix with the ulong parameter guarantees that the correct type is called, for example:

MyMethod(5);    // Calling the method with the int parameter
MyMethod(5UL);  // Calling the method with the ulong parameter

Conversions

There is a predefined implicit conversion from ulong to float, double, or decimal.

There is no implicit conversion from ulong to any integral type. For example, the following statement will produce a compilation error without an explicit cast:

long myLong = 8UL;   // Error: no implicit conversion from ulong

There is a predefined implicit conversion from byte, ushort, uint, or char to ulong.

Also, there is no implicit conversion from floating-point types to ulong. For example, the following statement generates a compiler error unless an explicit cast is used:

ulong x = 3.0;         // Error: no implicit conversion from double
ulong y = (ulong)3.0;  // OK: explicit conversion

For information on arithmetic expressions with mixed floating-point types and integral types, see float and double.

For more information on implicit numeric conversion rules, see the Implicit Numeric Conversions Table.

See Also

C# Keywords | Integral Types Table | Built-in Types Table | Implicit Numeric Conversions Table | Explicit Numeric Conversions Table


ushort

The ushort keyword denotes an integral data type that stores values according to the size and range shown in the following table.

Type | Range | Size | .NET Type
ushort | 0 to 65,535 | Unsigned 16-bit integer | System.UInt16

Literals

You can declare and initialize a ushort variable like this example:

ushort myShort = 65535;

In the preceding declaration, the integer literal 65535 is implicitly converted from int to ushort. If the integer literal exceeds the range of ushort, a compilation error will occur.

A cast must be used when calling overloaded methods. Consider, for example, the following overloaded methods that use ushort and int parameters:

public static void MyMethod(int i) {}
public static void MyMethod(ushort s) {}

Using the ushort cast guarantees that the correct type is called, for example:

MyMethod(5);          // Calling the method with the int parameter
MyMethod((ushort)5);  // Calling the method with the ushort parameter

Conversions

There is a predefined implicit conversion from ushort to int, uint, long, ulong, float, double, or decimal.

There is a predefined implicit conversion from byte or char to ushort. Otherwise a cast must be used to perform an explicit conversion. Consider for example, the following two ushort variables x and y:

ushort x = 5, y = 12;

The following assignment statement will produce a compilation error, because the arithmetic expression on the right-hand side of the assignment operator evaluates to int by default.

ushort z = x + y;   // Error: conversion from int to ushort

To fix this problem, use a cast:

ushort z = (ushort)(x + y);   // OK: explicit conversion

It is possible though to use the following statements, where the destination variable has the same storage size or a larger storage size:

int m = x + y;
long n = x + y;

Notice also that there is no implicit conversion from floating-point types to ushort. For example, the following statement generates a compiler error unless an explicit cast is used:

ushort x = 3.0;          // Error: no implicit conversion from double
ushort y = (ushort)3.0;  // OK: explicit conversion

For information on arithmetic expressions with mixed floating-point types and integral types, see float and double.

For more information on implicit numeric conversion rules, see the Implicit Numeric Conversions Table.

See Also

C# Keywords | Integral Types Table | Built-in Types Table | Implicit Numeric Conversions Table | Explicit Numeric Conversions Table


Boxing and Unboxing

Boxing and unboxing enable value types to be treated as objects. Value types, including both struct types and built-in types, such as int, can be converted to and from the type object.

See Also

Reference Types | Value Types


Boxing Conversion

Boxing is an implicit conversion of a value type to the type object or to any interface type implemented by this value type. Boxing a value of a value allocates an object instance and copies the value into the new object.

Consider the following declaration of a value-type variable:

int i = 123;

The following statement implicitly applies the boxing operation on the variable i:

object o = i;

The result of this statement is creating an object o, on the stack, that references a value of the type int, on the heap. This value is a copy of the value-type value assigned to the variable i. The difference between the two variables, i and o, is illustrated in the following figure.

Boxing Conversion

It also possible, but never needed, to perform the boxing explicitly as in the following example:

int i = 123;
object o = (object) i;

Example

This example converts an integer variable i to an object o via boxing. Then the value stored in the variable i is changed from 123 to 456. The example shows that the object keeps the original copy of the contents, 123.

// Boxing an integer variable
using System;
class TestBoxing
{
   public static void Main()
   {
      int i = 123;
      object o = i; // Implicit boxing
      i = 456;      // Change the contents of i
      Console.WriteLine("The value-type value = {0}", i);
      Console.WriteLine("The object-type value = {0}", o);
   }
}

Output

The value-type value = 456
The object-type value = 123

See Also

Boxing and Unboxing | Unboxing Conversion


Unboxing Conversion

Unboxing is an explicit conversion from the type object to a value type or from an interface type to a value type that implements the interface. An unboxing operation consists of:

  • Checking the object instance to make sure it is a boxed value of the given value type.
  • Copying the value from the instance into the value-type variable.

The following statements demonstrate both boxing and unboxing operations:

int i = 123;          // A value type
object box = i;       // Boxing
int j = (int)box;     // Unboxing

The following figure demonstrates the result of the preceding statements.

Unboxing Conversion

For an unboxing conversion to a given value type to succeed at run time, the value of the source argument must be a reference to an object that was previously created by boxing a value of that value type. If the source argument is null or a reference to an incompatible object, an InvalidCastException is thrown.

Example

The following example demonstrates a case of invalid unboxing. By using try and catch, an error message is displayed when the error occurs.

// Example to show how incorrect unboxing leads to InvalidCastException
using System;
public class UnboxingTest
{
   public static void Main()
   {
      int intI = 123;

      //Boxing
      object o = intI;

      // Reference to incompatible object produces InvalidCastException
      try
      {
         int intJ = (short) o;
         Console.WriteLine("Unboxing OK.");
      }

      catch (InvalidCastException e)
      {
         Console.WriteLine("{0} Error: Incorrect unboxing.",e);
      }
   }
}

Output

System.InvalidCastException
   at UnboxingTest.Main() Error: Incorrect unboxing.

If you change the statement

int intJ = (short) o;

to:

int intJ = (int) o;

the conversion will be performed, and you will get the output Unboxing OK.

See Also

Boxing and Unboxing | Boxing Conversion


Reference Types

Variables of reference types, referred to as objects, store references to the actual data. This section introduces the following keywords used to declare reference types:

  • class
  • interface
  • delegate

This section also introduces the following built-in reference types:

  • object
  • string

See Also

C# Keywords | Types Categories | Value Types


class

Classes are declared using the keyword class. The declaration takes the form:

[attributes] [modifiers] class identifier [:base-list] { class-body }[;]

where:

attributes (Optional)
Additional declarative information. For more information on attributes and attribute classes, see §17. Attributes.
modifiers (Optional)
The allowed modifiers are new, abstract, sealed, and the four access modifiers.
identifier
The class name.
base-list (Optional)
A list that contains the one base class and any implemented interfaces, all separated by commas.
class-body
Declarations of the class members.

Remarks

Unlike C++, only single inheritance is allowed in C#. In other words, a class can inherit implementation from one base class only. However, a class can implement more that one interface. The following table shows examples of class inheritance and interface implementation:

Inheritance | Example
None | class ClassA { }
Single | class DerivedClass: BaseClass { }
None, implements two interfaces | class ImplClass: IFace1, IFace2 { }
Single, implements one interface | class ImplDerivedClass: BaseClass, IFace1 { }

The access levels protected and private are only allowed on nested classes.

A class can contain declarations of the following members:

  • Constructors (see §10.10 Instance constructors)
  • Destructors (see §10.11 Destructors)
  • Constants (see §10.3 Constants)
  • Fields (see §10.4 Fields)
  • Methods (see §10.5 Methods)
  • Properties (see §10.6 Properties)
  • Indexers (see §10.8 Indexers)
  • Operators (see §10.9 Operators)

The framework may not call a destructor when an object goes out of scope. Your program should explicitly execute code that must run before an object goes out of scope.

Example

The following example demonstrates declaring class fields, constructors, and methods. It also demonstrates object instantiation and printing instance data. In this example, two classes are declared, the Kid class, which contains two private fields (name and age) and two public methods. The second class is MainClass, used for data processing.

// class example
using System;
public class Kid
{
   private int age;
   private string name;

   // Default constructor
   public Kid()
   {
      name = "N/A";
   }

   // Constructor:
   public Kid(string name, int age)
   {
      this.name = name;
      this.age = age;
   }

   // Printing method:
   public void PrintKid()
   {
      Console.WriteLine("{0}, {1} years old.", name, age);
   }
}

public class MainClass
{
   public static void Main()
   {
      // Create objects
      // Objects must be created using the new operator:
      Kid kid1 = new Kid("Craig", 11);
      Kid kid2 = new Kid("Sally", 10);

      // Create an object using the default constructor:
      Kid kid3 = new Kid();

      // Display results:
      Console.Write("Kid #1: ");
      kid1.PrintKid();
      Console.Write("Kid #2: ");
      kid2.PrintKid();
      Console.Write("Kid #3: ");
      kid3.PrintKid();
   }
}

Output

Kid #1: Craig, 11 years old.
Kid #2: Sally, 10 years old.
Kid #3: N/A, 0 years old.

Notice, in the preceding example, that the private fields (name and age) can only be accessed through the public methods of the Kid class. For example, you cannot print the kid's name, from the Main method, using a statement like this:

Console.Write(kid1.name);   // Error

However, it is possible to do that if the Main method is a member of the Kid class.

Notice also that the private modifier is the default modifier for class members. If you remove the modifier, you still get private members. For more information on access level modifiers, see Access Modifiers.

Notice also that for the object created using the default constructor (kid3), the age field was initialized to zero.

For more information on classes, see §10. Classes.

See Also

C# Keywords | Reference Types | §21.10 Classes Grammar | interface


delegate

A delegate declaration defines a reference type that can be used to encapsulate a method with a specific signature. A delegate instance encapsulates a static or an instance method. Delegates are roughly similar to function pointers in C++; however, delegates are type-safe and secure.

The delegate declaration takes the form:

[attributes] [modifiers] delegate result-type identifier ([formal-parameters]);

where:

attributes (Optional)
Additional declarative information. For more information on attributes and attribute classes, see §17. Attributes.
modifiers (Optional)
The allowed modifiers are new and the four access modifiers.
result-type
The result type, which matches the return type of the method.
identifier
The delegate name.
formal-parameters (Optional)
Parameter list.

For more information on delegates, see §15. Delegates.

Example

In the following example, one delegate is mapped to both static and instance methods and returns specific information from each.

// Calling both static and instance methods from delegates
using System;

// delegate declaration
delegate int MyDelegate();

public class MyClass
{
   public int InstanceMethod ()
   {
      Console.WriteLine("A message from the instance method.");
      return 0;
   }

   static public int StaticMethod ()
   {
      Console.WriteLine("A message from the static method.");
      return 0;
   }
}

public class MainClass
{
   static public void Main ()
   {
      MyClass p = new MyClass();

      // Map the delegate to the instance method:
      MyDelegate d = new MyDelegate(p.InstanceMethod);

      // Call the instance method:
      d();

      // Map to the static method:
      d = new MyDelegate(MyClass.StaticMethod);

      // Call the static method:
      d();
   }
}

Output

A message from the instance method.
A message from the static method.

See Also

C# Keywords | Reference Types | §21.15 Delegates Grammar | Delegates Tutorial


interface

The interface keyword declares a reference type that has abstract members. The interface declaration takes the form:

[attributes] [modifiers] interface identifier [:base-list] {interface-body}[;]

where:

attributes (Optional)
Additional declarative information. For more information on attributes and attribute classes, see §17. Attributes.
modifiers (Optional)
The allowed modifiers are new and the four access modifiers.
identifier
The interface name.
base-list (Optional)
A list that contains one or more explicit base interfaces separated by commas.
interface-body
Declarations of the interface members.

Remarks

An interface can be a member of a namespace or a class and can contain signatures of the following members:

  • Methods (see §10.5 Methods)
  • Properties
  • Indexers

An interface can inherit from one or more base interfaces. In the following example, the interface IMyInterface inherits from two base interfaces, IBase1 and IBase2:

interface IMyInterface: IBase1, IBase2
{
   void MethodA();
   void MethodB();
}

Interfaces can be implemented by classes. The identifier of the implemented interface appears in the class base list. For example:

class Class1: Iface1, Iface2
{
   // class members
}

When a class base list contains a base class and interfaces, the base class comes first in the list. For example:

class ClassA: BaseClass, Iface1, Iface2
{
   // class members
}

For more information on interfaces, see §13. Interfaces.

For more information on properties and indexers, see Property Declaration and Indexer Declaration.

Example

The following example demonstrates interface implementation. In this example, the interface IPoint contains the property declaration, which is responsible for setting and getting the values of the fields. The class MyPoint contains the property implementation.

// Interface implementation
using System;
interface IPoint
{
   // Property signatures:
   int x
   {
      get;
      set;
   }

   int y
   {
      get;
      set;
   }
}

class MyPoint : IPoint
{
   // Fields:
   private int myX;
   private int myY;

   // Constructor:
   public MyPoint(int x, int y)
   {
      myX = x;
      myY = y;
   }

   // Property implementation:
   public int x
   {
      get
      {
         return myX;
      }

      set
      {
         myX = value;
      }
   }

   public int y
   {
      get
      {
         return myY;
      }
      set
      {
         myY = value;
      }
   }
}

class MainClass
{
   private static void PrintPoint(IPoint p)
   {
      Console.WriteLine("x={0}, y={1}", p.x, p.y);
   }

   public static void Main()
   {
      MyPoint p = new MyPoint(2,3);
      Console.Write("My Point: ");
      PrintPoint(p);
   }
}

Output

My Point: x=2, y=3

See Also

C# Keywords | Reference Types | Properties | Indexers | §21.13 Interfaces Grammar | Explicit Interface Implementation Tutorial | class


object

The object type is based on System.Object in the .NET Framework. You can assign values of any type to variables of type object.

All data types, predefined and user-defined, inherit from the System.Object class. The object data type is the type to and from which objects are boxed.

Example

The following sample shows how variables of type object can accept values of any data type and how variables of type object can use methods on System.Object from the .NET Framework.

using System;
public class MyClass1
{
   public int i = 10;
}

public class MyClass2
{
   public static void Main()
   {
      object a;
      a = 1;   // an example of boxing
      Console.WriteLine(a);
      Console.WriteLine(a.GetType());
      Console.WriteLine(a.ToString());
      Console.WriteLine();

      a = new MyClass1 ();
      MyClass1 ref_MyClass1;
      ref_MyClass1 = (MyClass1)a;
      Console.WriteLine(ref_MyClass1.i);
   }
}

Output

1
Type: Int32
1

10

See Also

C# Keywords | Reference Types | Value Types


string

The string type represents a string of Unicode characters. string is an alias for System.String in the .NET Framework.

Although string is a reference type, the equality operators (== and !=) are defined to compare the values of string objects, not references (§7.9.7 String equality operators). This makes testing for string equality more intuitive.

string a = "hello";
string b = new string( "hello" );
Console.WriteLine( a == b );  // output: True -- same value
Console.WriteLine( (object)a == b ); //  False -- different objects

The + operator concatenates strings:

string a = "good " + "morning";

The [] operator accesses individual characters of a string:

char x = "test"[2];  // x = 's';

String literals are of type string and can be written in two forms, quoted and @-quoted. Quoted string literals are enclosed in double quotation marks ("):

"good morning"   // a string literal

and can contain any character literal, including escape sequences:

string a = "\\\u0066\n";  // backslash, letter f, new line

@-quoted string literals start with @ and are enclosed in double quotation marks. For example:

@"good morning"  // a string literal

The advantage of @-quoting is that escape sequences are not processed, which makes it easy to write, for example, a fully qualified file name:

@"c:\Docs\Source\a.txt"  // rather than "c:\\Docs\\Source\\a.txt"

To include a double quotation mark in an @-quoted string, double it:

@"""Ahoy!"" cried the captain." // "Ahoy!" cried the captain.

Another use of the @ symbol is to use referenced (/reference) identifiers that happen to be C# keywords. For more information, see §2.5.1 Identifiers.

Example

using System;
class test
{
   public static void Main( String[] args )
   {
      string a = "\u0068ello ";
      string b = "world";
      Console.WriteLine( a + b );
      Console.WriteLine( a + b == "hello world" );
   }
}

Output

hello world
True

See Also

C# Keywords | Reference Types | Value Types


void

When used as the return type for a method, void specifies that the method does not return a value.

void is not allowed in a method's parameter list. A method that takes no parameters and returns no value is declared as follows:

void MyMethod();

See Also

C# Keywords | Reference Types | Value Types


Types Reference Tables

The following reference tables summarize the C# types:

  • Built-in types
  • Integral types
  • Floating-point types
  • Default values
  • Value types
  • Implicit numeric conversions
  • Explicit numeric conversions

See Also

Reference Types | Value Types | Pointer Types


Built-in Types Table

The following table shows the keywords for built-in C# types, which are aliases of predefined types in the System namespace.

C# Type | .NET Type
bool | System.Boolean
byte | System.Byte
sbyte | System.SByte
char | System.Char
decimal | System.Decimal
double | System.Double
float | System.Single
int | System.Int32
uint | System.UInt32
long | System.Int64
ulong | System.UInt64
object | System.Object
short | System.Int16
ushort | System.UInt16
string | System.String

Remarks

All the types in the table, except object and string, are referred to as simple types.

The C# type keywords and their aliases are interchangeable. For example, you can declare an integer variable by using either of the following declarations:

int x = 123;
System.Int32 x = 123;

To display the actual type for any C# type use the system method GetType(). For example, the following statement displays the system alias that represents the type of myVariable:

Console.WriteLine(myVariable.GetType());

You can also use the method typeof.

See Also

C# Keywords | Value Types | Default Values Table | Types Reference Tables


Integral Types Table

The following table shows the sizes and ranges of the integral types, which constitute a subset of simple types.

Type | Range | Size
sbyte | -128 to 127 | Signed 8-bit integer
byte | 0 to 255 | Unsigned 8-bit integer
char | U+0000 to U+ffff | Unicode 16-bit character
short | -32,768 to 32,767 | Signed 16-bit integer
ushort | 0 to 65,535 | Unsigned 16-bit integer
int | -2,147,483,648 to 2,147,483,647 | Signed 32-bit integer
uint | 0 to 4,294,967,295 | Unsigned 32-bit integer
long | –9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 | Signed 64-bit integer
ulong | 0 to 18,446,744,073,709,551,615 | Unsigned 64-bit integer

Remarks

If the value represented by an integer literal exceeds the range of ulong, a compilation error will occur.

See Also

C# Keywords | Built-in Types Table | Floating-Point Types Table | Default Values Table | Types Reference Tables


Floating-Point Types Table

The following table shows the precision and approximate ranges for the floating-point types.

Type | Approximate range | Precision
float | ±1.5 × 10-45 to ±3.4 × 1038 | 7 digits
double | ±5.0 × 10-324 to ±1.7 × 10308 | 15-16 digits

See Also

Default Values Table | Built-in Types Table | Integral Types Table | Types Reference Tables | decimal


Default Values Table

The following table shows the default values of value types returned by the default constructors. Default constructors are invoked by using the new operator.

Value type | Default value
bool | false
byte | 0
char | '\0'
decimal | 0.0M
double | 0.0D
enum | The value produced by the expression (E)0, where E is the enum identifier.
float | 0.0F
int | 0
long | 0L
sbyte | 0
short | 0
struct | The value produced by setting all value-type fields to their default values and all reference-type fields to null.
uint | 0
ulong | 0
ushort | 0

See Also

Value Types | Built-in Types Table | Types Reference Tables


Value Types Table

The following table lists the C# value types by category.

Value type | Category
bool | Boolean
byte | Unsigned, numeric, integral
char | Unsigned, numeric, integral
decimal | Numeric, decimal
double | Numeric, floating-point
enum | Enumeration
float | Numeric, floating-point
int | Signed, numeric, integral
long | Signed, numeric, integral
sbyte | Signed, numeric, integral
short | Signed, numeric, integral
struct | User-defined structure
uint | Unsigned, numeric, integral
ulong | Unsigned, numeric, integral
ushort | Unsigned, numeric, integral

See Also

Value Types | Types Reference Tables


Implicit Numeric Conversions Table

The following table shows the predefined implicit numeric conversions. Implicit conversions might occur in many situations, including method invoking and assignment statements.

From | To
sbyte | short, int, long, float, double, or decimal
byte | short, ushort, int, uint, long, ulong, float, double, or decimal
short | int, long, float, double, or decimal
ushort | int, uint, long, ulong, float, double, or decimal
int | long, float, double, or decimal
uint | long, ulong, float, double, or decimal
long | float, double, or decimal
char | ushort, int, uint, long, ulong, float, double, or decimal
float | double
ulong | float, double, or decimal

Remarks

  • The conversions from int, uint, or long to float and from long to double may cause a loss of precision, but not a loss of magnitude.
  • There are no implicit conversions to the char type.
  • There are no implicit conversions between floating-point types and the decimal type.
  • A constant expression of type int can be converted to sbyte, byte, short, ushort, uint, or ulong, provided the value of the constant expression is within the range of the destination type.

For more information on implicit conversion, see §6.1 Implicit conversions.

For more information on constant expressions, see §7.15 Constant expressions.

See Also

Integral Types Table | Built-in Types Table | Explicit Numeric Conversions Table


Explicit Numeric Conversions Table

Explicit numeric conversion is used to convert any numeric type to any other numeric type, for which there is no implicit conversion, by using a cast expression. The following table shows these conversions.

From | To
sbyte | byte, ushort, uint, ulong, or char
byte | sbyte or char
short | sbyte, byte, ushort, uint, ulong, or char
ushort | sbyte, byte, short, or char
int | sbyte, byte, short, ushort, uint, ulong, or char
uint | sbyte, byte, short, ushort, int, or char
long | sbyte, byte, short, ushort, int, uint, ulong, or char
ulong | sbyte, byte, short, ushort, int, uint, long, or char
char | sbyte, byte, or short
float | sbyte, byte, short, ushort, int, uint, long, ulong, char, or decimal
double | sbyte, byte, short, ushort, int, uint, long, ulong, char, float, or decimal
decimal | sbyte, byte, short, ushort, int, uint, long, ulong, char, float, or double

Remarks

  • The explicit numeric conversion may cause loss of precision or result in throwing exceptions.
  • When you convert a float, double, or decimal value to an integral type, this value is rounded towards zero to the nearest integral value. If the resulting integral value is outside the range of the destination type, an InvalidCastException is thrown.
  • When you convert double to float, the double value is rounded to the nearest float value. If the double value is too small or too large to fit into the destination type, the result will be zero or infinity.
  • When you convert float or double to decimal, the source value is converted to decimal representation and rounded to the nearest number after the 28th decimal place if required. Depending on the value of the source value, one of the following results may occur:
    • If the source value is too small to be represented as a decimal, the result becomes zero.
    • If the source value is NaN (not a number), infinity, or too large to be represented as a decimal, an InvalidCastException is thrown.
  • When you convert decimal to float or double, the decimal value is rounded to the nearest double or float value.

For more information on explicit conversion, see §6.2 Explicit conversions.

See Also

Integral Types Table | Built-in Types Table | Implicit Numeric Conversions Table


Modifiers

Modifiers are used to modify declarations of types and type members. This section introduces the C# modifiers:

Modifier | Purpose
Access Modifiers
  • public
  • private
  • internal
  • protected
 Specify the declared accessibility of types and type members.
abstract | Indicate that a class is intended only to be a base class of other classes.
const | Specify that the value of the field or the local variable cannot be modified.
event | Declares an event.
extern | Indicate that the method is implemented externally.
override | Provide a new implementation of a virtual member inherited from a base class.
readonly | Declare a field that can only be assigned values as part of the declaration or in a constructor in the same class.
sealed | Specify that a class cannot be inherited.
static | Declare a member that belongs to the type itself rather than to a specific object.
virtual | Declare a method or an accessor whose implementation can be changed by an overriding member in a derived class.

See Also

C# Keywords


Access Modifiers

Access modifiers are keywords used to specify the declared accessibility of a member or a type. This section introduces the four access modifiers:

  • public
  • protected
  • internal
  • private

The following five accessibility levels can be specified using the access modifiers:

public protected internal internal protected private

This section also introduces the following topics:

  • Accessibility Levels
  • Accessibility Domain
  • Restrictions on Using Accessibility Levels

See Also

C# Keywords | Modifiers


Accessibility Levels

When access is allowed to a member, it said to be accessible. Otherwise, it is inaccessible. Use the access modifiers, public, protected, internal, or private, to specify one of the following declared accessibilities for members.

Declared accessibility | Meaning
public | Access is not restricted.
protected | Access is limited to the containing class or types derived from the containing class.
internal | Access is limited to the current project.
protected internal | Access is limited to the current project or types derived from the containing class.
private | Access is limited to the containing type.

Only one access modifier is allowed for a member or type, except for the protected internal combination.

Access modifiers are not allowed on namespaces. Namespaces have no access restrictions.

Depending on the context in which a member declaration takes place, only certain declared accessibilities are permitted. If no access modifier is specified in a member declaration, a default accessibility is used.

Top-level types, which not nested into other types, can only have internal or public accessibility. The default accessibility for these types is internal.

Nested types, which are members of other types, can have declared accessibilities as indicated in the following table.

Members of | Default member accessibility | Allowed declared accessibility of the member
enum | public | None
class | private | public protected internal private protected internal
interface | public | None
struct | private | public internal private

The accessibility of a nested type depends on its accessibility domain, which is determined by both the declared accessibility of the member and the accessibility domain of the immediately containing type. However, the accessibility domain of a nested type cannot exceed that of the containing type.

See Also

Access Modifiers | Accessibility Domain | Restrictions on Using Accessibility Levels


Accessibility Domain

The accessibility domain of a member specifies where, in the program sections, a member can be referenced. If the member is nested within another type, its accessibility domain is determined by both the accessibility level of the member and the accessibility domain of the immediately containing type.

The accessibility domain of a top-level type is at least the program text of the project in which it is declared. That is, the entire source files of this project. The accessibility domain of a nested type is at least the program text of the type in which it is declared. That is, the type body, including any nested types. The accessibility domain of a nested type never exceeds that of the containing type. These concepts are demonstrated in the following example.

For more information, see §3.3.2 Accessibility domains.

Example

This example contains a top-level type, T1, and two nested classes, M1 and M2. The classes contain fields with different declared accessibilities. In the Main method, a comment follows each statement to indicate the accessibility domain of each member. Notice that the statements that attempt to reference the inaccessible members are commented out. If you want to see the compiler errors caused by referencing an inaccessible member, remove the comments one at a time.

using System;
namespace MyNameSpace
{
   public class T1
   {
      public static int myPublicInt;
      internal static int myInternalInt;
      private static int myPrivateInt = 0;

      public class M1
      {
         public static int myPublicInt;
         internal static int myInternalInt;
         private static int myPrivateInt = 0;
      }

      private class M2
      {
         public static int myPublicInt = 0;
         internal static int myInternalInt = 0;
         private static int myPrivateInt = 0;
      }
   }

   public class MainClass
   {
      public static int Main()
      {
         // Access to T1 fields:
         T1.myPublicInt = 1;      // Access is unlimited
         T1.myInternalInt = 2;    // Accessible only in current project
         // T1.myPrivateInt = 3;  // Error: inaccessible outside T1

         // Access to the M1 fields:
         T1.M1.myPublicInt = 1;     // Access is unlimited
         T1.M1.myInternalInt = 2;   // Accessible only in current project
         // T1.M1.myPrivateInt = 3; // Error: inaccessible outside M1

         // Access to the M2 fields:
         // T1.M2.myPublicInt = 1;   // Error: inaccessible outside T1
         // T1.M2.myInternalInt = 2; // Error: inaccessible outside T1
         // T1.M2.myPrivateInt = 3;  // Error: inaccessible outside M2

         return 0;
      }
   }
}

See Also

Access Modifiers | Accessibility Levels | Restrictions on Using Accessibility Levels


Restrictions on Using Accessibility Levels

When you declare a type, it is essential to see if that type has to be at least as accessible as another member or type. For example, the direct base class must be at least as accessible as the derived class. The following declarations will result in a compiler error, because the base class BaseClass is less accessible than MyClass:

class BaseClass {...}
public class MyClass: BaseClass {...} // Error

The following table summarizes the restrictions on using declared accessibility levels.

Context | Remarks
Classes | The direct base class of a class type must be at least as accessible as the class type itself.
Interfaces
  • The explicit base interfaces of an interface type must be at least as accessible as the interface type itself.
Delegates | The return type and parameter types of a delegate type must be at least as accessible as the delegate type itself.
Constants | The type of a constant must be at least as accessible as the constant itself.
Fields | The type of a field must be at least as accessible as the field itself.
Methods | The return type and parameter types of a method must be at least as accessible as the method itself.
Properties | The type of a property must be at least as accessible as the property itself.
Events | The type of an event must be at least as accessible as the event itself.
Indexers | The type and parameter types of an indexer must be at least as accessible as the indexer itself.
Operators | The return type and parameter types of an operator must be at least as accessible as the operator itself.
Constructors | The parameter types of a constructor must be at least as accessible as the constructor itself.

Example

The following example contains erroneous declarations of different types. The comment following each declaration indicates the expected compiler error.

// Restrictions on using declared accessibility
using System;

// A delegate
delegate int MyDelegate();
class B
{
   // A private method:
   int MyPrivateMethod()
   {
      return 0;
   }
}

public class A
{
   // Fields
   public B myField = new B();   // Error: The type B is less accessible
               // than the field A.myField
   // Constants
   public readonly B myConst = new B();
   // Error: The type B is less accessible than the constant A.myConst

   // Methods
   public B MyMethod()
   {
      return new B();
   }   // Error: The type B is less accessible than the method A.MyMethod

   // Properties:
   public  B MyProp
   {
      set
      {
      }
   }   // Error: The type B is less accessible than the property A.MyProp

   // Delegates:
   MyDelegate d = new MyDelegate(B.MyMethod);
   // Error: The parameter B.MyMethod is not accessible due to
   // protection level

   // Operators:
   public static B operator + (A m1, B m2)
   {
      return new B();   // Error: The type B is less accessible than the
            // operator A.operator +(A,B)
   }
}

See Also

Access Modifiers | Accessibility Domain | Accessibility Levels


internal

The internal keyword is an access modifier for types and type members. Internal members are accessible only within files in the same assembly. For more information on assemblies, see Components and Assemblies.

A common use of internal access is in component-based development because it enables a group of components to cooperate in a private manner without being exposed to the rest of the application code. For example, a framework for building graphical user interfaces could provide Control and Form classes that cooperate using members with internal access. Since these members are internal, they are not exposed to code that is using the framework.

It is an error to reference a member with internal access outside the assembly within which it was defined.

For a comparison of internal with the other access modifiers, see Accessibility Levels.

Example

This example contains two files, Assembly1.cs and Assembly2.cs. The first file contains an internal base class, BaseClass. In the second file, an attempt to access the member of the base class will produce an error.

File Assembly1.cs:

// compile with /target:library
internal class BaseClass
{
   public static int IntM = 0;
}

File Assembly2.cs

// compile with /target:exe /reference:Assembly1.dll
class TestAccess
{
   public static void Main()
{
      BaseClass myBase = new BaseClass();   // error, BaseClass not visible outside assembly
   }
}

See Also

C# Keywords | Access Modifiers | Accessibility Levels | Modifiers


private

The private keyword is a member access modifier. Private access is the least permissive access level. Private members are accessible only within the body of the class or the struct in which they are declared.

Nested types in the same body can also access those private members.

It is a compile-time error to reference a private member outside the class or the struct in which it is declared.

For a comparison of private with the other access modifiers, see Accessibility Levels.

Example

In this example, the Employee class contains a public member, Name, and a private member, Salary. The public member can be accessed directly, while the private member must be accessed through the public method AccessSalary().

// Private members
using System;
class Employee
{
   public string name = "xx";
   double salary = 100.00;   // private access by default
   public double AccessSalary() {
      return salary;
   }
}

class MainClass
{
   public static void Main()
   {
      Employee e = new Employee();

      // Accessing the public field:
      string n = e.name;

      // Accessing the private field:
      double s = e.AccessSalary();
   }
}

In the preceding example, if you attempt to access the private members directly, by using a statement like this:

double s = e.salary;

you will get the error message:

'Employee.Salary' is inaccessible due to its protection level.

See Also

C# Keywords | Access Modifiers | Accessibility Levels | Modifiers


protected

The protected keyword is a member access modifier. A protected member is accessible from within the class in which it is declared, and from within any class derived from the class that declared this member.

A protected member of a base class is accessible in a derived class only if the access takes place through the derived class type. For example, consider the following code segment:

class A
{
   protected int x = 123;
}

class B : A
{
   void F()
   {
      A a = new A();
      B b = new B();
      a.x = 10;   // Error
      b.x = 10;   // OK
   }
}

The statement a.x =10 generates an error because A is not derived from B.

Struct members cannot be protected because the struct cannot be inherited.

It is an error to reference a protected member from a class, which is not derived from the protected member's class.

For more information on protected members, see §3.3.3 Protected access.

For a comparison of protected with the other access modifiers, see Accessibility Levels.

Example

In this example, the class MyDerivedC is derived from MyClass; therefore, you can access the protected members of the base class directly from the derived class.

using System;
class MyClass
{
   protected int x;
   protected int y;
}

class MyDerivedC: MyClass
{
   public static void Main()
   {
      MyDerivedC mC = new MyDerivedC();

      // Direct access to protected members:
      mC.x = 10;
      mC.y = 15;
      Console.WriteLine("x = {0}, y = {1}", mC.x, mC.y);
   }
}

Output

x = 10, y = 15

If you change the access levels of x and y to private, the compiler will issue the error messages:

'MyClass.y' is inaccessible due to its protection level.
'MyClass.x' is inaccessible due to its protection level.

See Also

C# Keywords | Access Modifiers | Accessibility Levels | Modifiers


public

The public keyword is an access modifier for types and type members. Public access is the most permissive access level. There are no restrictions on accessing public members.

For a comparison of public with the other access modifiers, see Accessibility Levels.

Example

In the following example, two classes are declared, MyClass1 and MyClass2. The public members x and y of the MyClass1 are accessed directly from MyClass2.

// Public access
using System;
class MyClass1
{
   public int x;
   public int y;
}

class MyClass2
{
   public static void Main()
   {
      MyClass1 mC = new MyClass1();

      // Direct access to public members:
      mC.x = 10;
      mC.y = 15;
      Console.WriteLine("x = {0}, y = {1}", mC.x, mC.y);
   }
}

Output

x = 10, y = 15

If you change the public access level to private or protected, you will get the error message:

'MyClass1.y' is inaccessible due to its protection level.

See Also

C# Keywords | Access Modifiers | Accessibility Levels | Modifiers


abstract

The abstract modifier can be used with classes, methods, and properties.

Use the abstract modifier in a class declaration to indicate that a class is intended only to be a base class of other classes.

Abstract classes have the following features:

  • An abstract class cannot be instantiated.
  • An abstract class may contain abstract methods and accessors.
  • It is not possible to modify an abstract class with the sealed modifier, which means that the class cannot be inherited.
  • A non-abstract class derived from an abstract class must include actual implementations of all inherited abstract methods and accessors.

Use the abstract modifier in a method or property declaration to indicate that the method or property does not contain implementation.

  • Abstract methods have the following features:
    • An abstract method is implicitly a virtual method.
    • Abstract method declarations are only permitted in abstract classes.
    • Because an abstract method declaration provides no actual implementation, there is no method body; the method declaration simply ends with a semicolon and there are no braces ({ })following the signature. For example:
    public abstract void MyMethod();
    • The implementation is provided by an overriding method, which is a member of a non-abstract class.
    • It is an error to use the static, virtual, or override modifiers in an abstract method declaration.

Abstract properties behave like abstract methods, except for the differences in declaration and invocation syntax.

  • It is an error to use the abstract modifier on a static property.
  • An abstract inherited property can be overridden in a derived class by including a property declaration that uses the override modifier.

An abstract class must provide implementation for all interface members.

An abstract class that implements an interface might map the interface methods onto abstract methods. For example:

interface I
{
   void M();
}
abstract class C: I
{
   public abstract void M();
}

For more information, see §10.1.1.1 Abstract classes and §10.5.5 Abstract methods.

Example

In this example, the class MyDerivedC is derived from an abstract class MyBaseC. The abstract class contains an abstract method, MyMethod(), and two abstract properties, GetX() and GetY().

// Abstract Classes
using System;
abstract class MyBaseC   // Abstract class
{
   protected int x = 100;
   protected int y = 150;
   public abstract void MyMethod();   // Abstract method

   public abstract int GetX   // Abstract property
   {
      get;
   }

   public abstract int GetY   // Abstract property
   {
      get;
   }
}

class MyDerivedC: MyBaseC
{
   public override void MyMethod()
   {
      x++;
      y++;
   }

   public override int GetX   // overriding property
   {
      get
      {
         return x+10;
      }
   }

   public override int GetY   // overriding property
   {
      get
      {
         return y+10;
      }
   }

   public static void Main()
   {
      MyDerivedC mC = new MyDerivedC();
      mC.MyMethod();
      Console.WriteLine("x = {0}, y = {1}", mC.GetX, mC.GetY);
   }
}

Output

x = 111, y = 161

In the preceding example, if you attempt to instantiate the abstract class by using a statement like this:

MyBaseC mC1 = new MyBaseC();   // Error

you will get the following error message:

Cannot create an instance of the abstract class 'MyBaseC'.

See Also

virtual | override | C# Keywords | Modifiers


const

The keyword const is used to modify a declaration of a field or local variable. It specifies that the value of the field or the local variable cannot be modified. A constant declaration introduces one or more constants of a given type. The declaration takes the form:

[attributes] [modifiers] const type declarators;

where:

attributes (optional)
Optional declarative information. For more information on attributes and attribute classes, see C# Attributes.
modifiers (optional)
Optional modifiers that include the new modifier and one of the four access modifiers.
type
One of the types: byte, char, short, int, long, float, double, decimal, bool, string, an enum type, or a reference type.
declarators
A comma-separated list of declarators. A declarator takes the form:

identifier = constant-expression

The attributes and modifiers apply to all of the members declared by the constant declaration.

The type of a constant declaration specifies the type of the members introduced by the declaration. A constant expression must yield a value of the target type, or of a type that can be implicitly converted to the target type.

A constant expression is an expression that can be fully evaluated at compile time. Therefore, the only possible values for constants of reference types are string and null.

Remarks

The constant declaration can declare multiple constants, for example:

public const double x = 1.0, y = 2.0, z = 3.0;

The static modifier is not allowed in a constant declaration.

A constant can participate in a constant expression, for example:

public const int c1 = 5.0;
public const int c2 = c1 + 100;

Example

// Constants
using System;
public class ConstTest
{
   class MyClass
   {
      public int x;
      public int y;
      public const int c1 = 5;
      public const int c2 = c1 + 5;

      public MyClass(int p1, int p2)
      {
         x = p1;
         y = p2;
      }
   }

   public static void Main()
   {
      MyClass mC = new MyClass(11, 22);
      Console.WriteLine("x = {0}, y = {1}", mC.x, mC.y);
      Console.WriteLine("c1 = {0}, c2 = {1}", MyClass.c1, MyClass.c2 );
   }
}

Output

x = 11, y = 22
c1 = 5, c2 = 10

Example

This example demonstrates using constants as local variables.

using System;
public class TestClass
{
   public static void Main()
   {
      const int c = 707;
      Console.WriteLine("My local constant = {0}", c);
   }
}

Output

My local constant = 707

See Also

C# Keywords | Modifiers


event

Specifies an event.

[attributes] [modifiers] event type declarator;
[attributes] [modifiers] event type member-name {accessor-declarations};

where:

accessor-declarations
Declaration of the property accessors, which are used to read and write the property.
attributes (optional)
Optional declarative information. For more information on attributes and attribute classes, see C# Attributes.
declarator
The name of the event.
member-name
The name of the event property.
modifiers (optional)
Optional modifiers that include new, static, and one of the four access modifiers.
type
The delegate to which you want to associate this event.

Remarks

The event keyword lets you specify a delegate that will be called upon the occurrence of some "event" in your code. The delegate can have one or more associated methods that will be called when your code indicates that the event has occurred. An event in one program can be made available to other programs that target the .NET runtime.

The following steps must be taken in order to create and use C# events:

  1. Create or identify a delegate. If you are defining your own event, you must also ensure that there is a delegate to use with the event keyword. If the event is predefined, in the .NET Framework, for example, then consumers of the event need only know the name of the delegate.
  2. Create a class that contains:
    • An event created from the delegate.
    • (optional) A method that verifies that an instance of the delegate declared with the event keyword exists. Otherwise, this logic must be placed in the code that fires the event.
    • Methods that call the event. These methods can be overrides of some base class functionality.

    This class defines the event.

  3. Define one or more classes that connect methods to the event. Each of these classes will include:
    • A constructor declared to take a parameter of the type class that contains the event declaration. This constructor will contain the code to associate one or more methods with the event.
    • The definition of the method(s) that will be associated with the event.
  4. Use the event:
    • Create an object of the class that contains the event declaration.
    • Create an object of the class that contains the event definition, using the constructor that you defined.

For more information on events, see:

  • §10.7 Events
  • Events Tutorial
  • Events and Delegates

Examples

A basic example of defining and using events can be found in the Events Tutorial. The following examples in this topic also demonstrate events.

Example | Comment
Example 1 | Declaring an event in an interface and implementing it in a class.
Example 2 | Using a hash table to store event instances.
Example 3 | Implementing, via event properties, two interfaces that have an event with the same name.

Example 1

This example shows that it is possible to declare an event in an interface and implement it in a class:

using System;
public delegate void MyDelegate();   // delegate declaration

public interface I
{
   event MyDelegate MyEvent;
   void FireAway();
}

public class MyClass: I
{
   public event MyDelegate MyEvent;

   public void FireAway()
   {
      if (MyEvent != null)
         MyEvent();
   }
}

public class MainClass
{
   static private void f()
   {
      Console.WriteLine("This is called when the event fires.");
   }

   static public void Main ()
   {
      I i = new MyClass();

      i.MyEvent += new MyDelegate(f);
      i.FireAway();
   }
}

Example 2

One use for event properties is for exposing a large number of properties without allocating a field for each event, but instead using a hash table to store the event instances. This is only useful if you have a very large number of events, but you expect most of the events will not be implemented.

using System;
using System.Collections;

public delegate void MyDelegate1(int i);
public delegate void MyDelegate2(string s);
public delegate void MyDelegate3(int i, object o);
public delegate void MyDelegate4();

public class PropertyEventsSample
{
   private Hashtable eventTable = new Hashtable();

   public event MyDelegate1 Event1
   {
      get
      {
         return (MyDelegate1) eventTable["Event1"];
      }
      set
      {
         eventTable["Event1"] = value;
      }
   }

   public event MyDelegate1 Event2
   {
      get
      {
         return (MyDelegate1) eventTable["Event2"];
      }
      set
      {
         eventTable["Event2"] = value;
      }
   }

   public event MyDelegate2 Event3
   {
      get
      {
         return (MyDelegate2) eventTable["Event3"];
      }
      set
      {
         eventTable["Event3"] = value;
      }
   }

   public event MyDelegate3 Event4
   {
      get
      {
         return (MyDelegate3) eventTable["Event4"];
      }
      set
      {
         eventTable["Event4"] = value;
      }
   }

   public event MyDelegate3 Event5
   {
      get
      {
         return (MyDelegate3) eventTable["Event5"];
      }
      set
      {
         eventTable["Event5"] = value;
      }
   }

   public event MyDelegate4 Event6
   {
      get
      {
         return (MyDelegate4) eventTable["Event6"];
      }
      set
      {
         eventTable["Event6"] = value;
      }
   }
}

public class MyClass
{
   public static void Main()
   {
   }
}

Example 3

Another use for event properties covers the situation where you are implementing two interfaces, each with an event of the same name. In such a case, you must use an explicit implementation event property:

using System;

public delegate void MyDelegate1();

public interface I1
{
   event MyDelegate1 MyEvent;
}

public delegate int MyDelegate2(string s);

public interface I2
{
   event MyDelegate2 MyEvent;
}

public class ExplicitEventsSample: I1, I2
{
   public event MyDelegate1 MyEvent;  // normal implementation of I1.MyEvent.

   event MyDelegate2 I2.MyEvent   // explicit implementation of I2.MyEvent
   {
      get
      {
         return MyEvent2Storage;
      }
      set
      {
         MyEvent2Storage = value;
      }
   }

   private MyDelegate2 MyEvent2Storage;  // underlying storage for I2.MyEvent.

   private void FireEvents()
   {
      if (MyEvent != null)
         MyEvent();
      if (MyEvent2Storage != null)
         MyEvent2Storage("hello");
   }
}

public class MyClass
{
   public static void Main()
   {
   }
}

See Also

C# Keywords | Modifiers


extern

Use the extern modifier in a method declaration to indicate that the method is implemented externally. A common use of the extern modifier is with the DllImport attribute. (See §19.6 The DllImport attribute for more information.)

It is an error to use the abstract and extern modifiers together to modify the same member. Using the extern modifier means that the method is implemented outside the C# code, while using the abstract modifier means that the method implementation is not provided in the class.

Because an external method declaration provides no actual implementation, there is no method body; the method declaration simply ends with a semicolon and there are no braces ({ }) following the signature. For example:

public static extern int MyMethod(int x);
Note The extern keyword is more limited in use than in C++. To compare with the C++ keyword, see Using extern to Specify Linkage in the C++ Language Reference.

For more information on external methods, see §10.5.6 External methods.

For more information on attributes, see §17. Attributes.

Example

In this example, the program receives a string from the user and displays it inside a message box. The program uses the MessageBox method imported from the User32.dll library.

// The extern modifier
using System;
using System.Runtime.InteropServices;
class MyClass
{
   [DllImport("User32.dll")]
   public static extern int MessageBox(int h, string m, string c, int type);

   public static int Main()
   {
      string myString;
      Console.Write("Enter your message: ");
      myString = Console.ReadLine();
      return MessageBox(0, myString, "My Message Box", 0);
   }
}

Sample Run

Enter your message: Where do you want to go today?

When the previous text is entered, a message box that contains the text will pop up on the screen.

Example

This example uses two files, CM.cs and Cmdll.c, to demonstrate extern. The C file is an external DLL that is invoked from within the C# program.

File: CM.cs

using System;
using System.Runtime.InteropServices;
public class MyClass
{
   [DllImport("Cmdll.dll")]
   public static extern int MyMethod(int x);
   public static void Main()
   {
      Console.WriteLine("MyMethod() returns {0}.", MyMethod(5));
   }
}

File: Cmdll.c

int __declspec(dllexport) MyMethod(int i)
{
   return i*10;
}

Compilation

To build the project, use the following steps:

  • Compile Cmdll.c to a DLL using the Visual C++ command line:
    cl /LD /MD Cmdll.c
  • Compile CM.cs using the command line:
    csc CM.cs
  • This will create the executable file CM.exe. When you run this program, MyMethod will pass the value 5 to the DLL file, which returns the value multiplied by 10.

Output

MyMethod() returns 50.

See Also

C# Keywords | Modifiers


override

Use the override modifier to modify a method or a property. An override method provides a new implementation of a member inherited from a base class. The method overridden by an override declaration is known as the overridden base method. The overridden base method must have the same signature as the override method.

You cannot override a non-virtual or static method. The overridden base method must be virtual, abstract, or override.

An override declaration cannot change the accessibility of the virtual method. Both the override method and the virtual method must have the same access level modifier.

You cannot use the following modifiers to modify an override method:

new static virtual abstract

An overriding property declaration must specify the exact same access modifier, type, and name as the inherited property, and the overridden property must be virtual, abstract, or override.

For more information on accessing the base class members, see §7.5.8 Base access.

For more information on overriding methods, see §10.5.4 Override methods.

Example

See the example for the virtual keyword.

From within the derived class that has an override method, you still can access the overridden base method that has the same name by using the base keyword. For example, if you have a virtual method MyMethod(), and an override method on a derived class, you can access the virtual method from the derived class by using the call:

base.MyMethod()

Compare this to the C++ way, where you use the scope resolution operator (::) and the base class name, for example:

My_Base_Class_Name::MyMethod()

Example

In this example, there is a base class, Square, and a derived class, Cube. Because the area of a cube is the sum of the areas of six squares, it is possible to calculate it by calling the Area() method on the base class.

// Calling overriden methods from the base class
using System;
class TestClass
{
   public class Square
   {
      public double x;

      // Constructor:
      public Square(double x)
      {
         this.x = x;
      }

      public virtual double Area()
      {
         return x*x;
      }
   }

   class Cube: Square
   {
      // Constructor:
      public Cube(double x): base(x)
      {
      }

      // Calling the Area base method:
      public override double Area()
      {
         return (6*(base.Area()));
      }
   }

   public static void Main()
   {
      double x = 5.2;
      Square s = new Square(x);
      Square c = new Cube(x);
      Console.WriteLine("Area of Square = {0}",
               double.Format(s.Area(), "0.00"));
      Console.WriteLine("Area of Cube = {0}",
               double.Format(c.Area(), "0.00"));
   }
}

Output

Area of Square = 27.04
Area of Cube = 162.24

See Also

abstract | virtual | C# Keywords | Modifiers | Versioning Tutorial


readonly

The readonly keyword is a modifier that you can use on fields. When a field declaration includes a readonly modifier, assignments to the fields introduced by the declaration can only occur as part of the declaration or in a constructor in the same class.

Remarks

You can assign a value to a readonly field only in the following contexts:

  • When the variable is initialized in the declaration, for example:
    public readonly int y = 5;
  • For an instance field, in the instance constructors of the class that contains the field declaration, or for a static field, in the static constructor of the class that contains the field declaration. These are also the only contexts in which it is valid to pass a readonly field as an out or ref parameter.

Example

// Readonly fields
using System;
public class ReadOnlyTest
{
   class MyClass
   {
      public int x;
      public readonly int y = 25; // Initialize a readonly field
      public readonly int z;

      public MyClass()
      {
         z = 24;   // Initialize a readonly instance field
      }

      public MyClass(int p1, int p2, int p3)
      {
         x = p1;
         y = p2;
         z = p3;
      }
   }

   public static void Main()
   {
      MyClass p1= new MyClass(11, 21, 32);   // OK
      Console.WriteLine("p1: x={0}, y={1}, z={2}" , p1.x, p1.y, p1.z);
      MyClass p2 = new MyClass();
      p2.x = 55;   // OK
      Console.WriteLine("p2: x={0}, y={1}, z={2}" , p2.x, p2.y, p2.z);
   }
}

Output

p1: x=11, y=21, z=32
p2: x=55, y=25, z=24

In the preceding example, if you use a statement like this:

p2.y = 66;        // Error

you will get the compiler error message:

The left-hand side of an assignment must be an l-value

which is the same error you get when you attempt to assign a value to a constant.

See Also

C# Keywords | Modifiers


sealed

A sealed class cannot be inherited. It is an error to use a sealed class as a base class. Use the sealed modifier in a class declaration to prevent accidental inheritance of the class.

Remarks

  • It is not permitted to use the abstract modifier with a sealed class.
  • Structs are implicitly sealed; therefore, they cannot be inherited.

Example

// Sealed classes
using System;
sealed class MyClass
{
   public int x;
   public int y;
}

class MainClass
{
   public static void Main()
   {
      MyClass mC = new MyClass();
      mC.x = 110;
      mC.y = 150;
      Console.WriteLine("x = {0}, y = {1}", mC.x, mC.y);
   }
}

Output

x = 110, y = 150

In the preceding example, if you attempt to inherit from the sealed class by using a statement like this:

class MyDerivedC: MyClass {}   // Error

you will get the error message:

'MyDerivedC' cannot inherit from sealed class 'MyBaseC'.

See Also

C# Keywords | Modifiers


static

Use the static modifier to declare a static member, which belongs to the type itself rather than to a specific object. The static modifier can be used with fields, methods, properties, operators, and constructors, but cannot be used with indexers, destructors, or types.

Remarks

  • A constant or type declaration is implicitly a static member.
  • A static member cannot be referenced through an instance. Instead, it is referenced through the type name. For example, consider the following class:
    public class MyBaseC
{
   public struct MyStruct {
      public static int x = 100;
   }
}

    To refer to the static member x, use the fully qualified name (unless it is accessible from the same scope):

    MyBaseC.MyStruct.x
  • While an instance of a class contains a separate copy of all instance fields of the class, there is only one copy of each static field.
  • It is not possible to use this to reference static methods or property accessors.
    Note The static keyword is more limited in use than in C++. To compare with the C++ keyword, see static in the C++ Language Reference.

To demonstrate instance members, consider a class that represents a company employee. Assume that the class contains a method to count employees and a field to store the number of employees. Both the method and the field do not belong to any instance employee. Instead they belong to the company class. Therefore, they should be declared as static members of the class.

For more information on constructors, see §10.10 Instance constructors.

Example

This example reads the name and ID of a new employee, increments the employee counter by one, and displays the information for the new employee as well as the new number of employees. For simplicity, this program reads the current number of employees from the keyboard. In a real application, this information should be read from a file.

// Static members
using System;
public class Employee
{
   public string id;
   public string name;

   public Employee ()
   {
   }

   public Employee (string name, string id)
   {
      this.name = name;
      this.id = id;
   }

   public static int employeeCounter;

   public static int AddEmployee()
   {
      return ++employeeCounter;
   }
}

class MainClass: Employee
{
   public static void Main()
   {
      Console.Write("Enter the employee's name: ");
      string name = Console.ReadLine();
      Console.Write("Enter the employee's ID: ");
      string id = Console.ReadLine();
      // Create the employee object:
      Employee e = new Employee (name, id);
      Console.Write("Enter the current number of employees: ");
      string n = Console.ReadLine();
      Employee.employeeCounter = Int32.FromString(n);
      Employee.AddEmployee();
      // Display the new information:
      Console.WriteLine("Name: {0}", e.name);
      Console.WriteLine("ID:   {0}", e.id);
      Console.WriteLine("New Number of Employees: {0}",
                         Employee.employeeCounter);
   }
}

Sample Run

Enter the employee's name: Tara Strahan
Enter the employee's ID: AF643G
Enter the current number of employees: 15
Name: Tara Strahan
ID:   AF643G
New Number of Employees: 16

See Also

C# Keywords | Modifiers


virtual

The virtual keyword is used to modify a method or property declaration, in which case the method or the property is called a virtual member. The implementation of a virtual member can be changed by an overriding member in a derived class.

When a virtual method is invoked, the run-time type of the object is checked for an overriding member. The overriding member in the most derived class is called, which might be the original member, if no derived class has overridden the member. (For more information on run-time type and most derived implementation, see §10.5.3 Virtual methods.)

By default, methods are non-virtual. You cannot override a non-virtual method.

You cannot use the virtual modifier with the following modifiers:

static abstract override

Virtual properties behave like abstract methods, except for the differences in declaration and invocation syntax.

  • It is an error to use the virtual modifier on a static property.
  • A virtual inherited property can be overridden in a derived class by including a property declaration that uses the override modifier.

For more information on virtual methods, see §10.5.3 Virtual methods.

Example

In this example, the class Dimensions contains the two coordinates x, y, and the Area() virtual method. Different shape classes such as Circle, Cylinder, and Sphere inherit the Dimensions class, and the surface area is calculated for each figure. Each derived class has it own override implementation of Area(). The program calculates and displays the proper area for each figure by invoking the proper implementation of Area() according to the object associated with the method.

// Virtual and override
using System;
class TestClass
{
   public class Dimensions
   {
      public const double pi = Math.PI;
      protected double x, y;
      public Dimensions()
      {
      }
      public Dimensions (double x, double y)
      {
         this.x = x;
         this.y = y;
      }

      public virtual double Area()
      {
         return x*y;
      }
   }

   public class Circle: Dimensions
   {
      public Circle(double r): base(r, 0)
      {
      }

      public override double Area()
      {
         return pi * x * x;
      }
   }

   class Sphere: Dimensions
   {
      public Sphere(double r): base(r, 0)
      {
      }

         public override double Area()
      {
         return 4 * pi * x * x;
      }
   }

   class Cylinder: Dimensions
   {
      public Cylinder(double r, double h): base(r, h)
      {
      }

      public override double Area()
      {
         return 2*pi*x*x + 2*pi*x*y;
      }
   }

   public static void Main()
   {
      double r = 3.0, h = 5.0;
      Dimensions c = new Circle(r);
      Dimensions s = new Sphere(r);
      Dimensions l = new Cylinder(r, h);
      // Display results:
      Console.WriteLine("Area of Circle   = {0}",
               double.Format(c.Area(), "0.00"));
      Console.WriteLine("Area of Sphere   = {0}",
               double.Format(s.Area(), "0.00"));
      Console.WriteLine("Area of Cylinder = {0}",
               double.Format(l.Area(), "0.00"));
   }
}

Output

Area of Circle   = 28.27
Area of Sphere   = 113.10
Area of Cylinder = 150.80

In the preceding example, notice that the inherited classes Circle, Sphere, and Cylinder are all using constructors that initialize the base class, for example:

public Cylinder(double r, double h): base(r, h) {}

This is analogous to the C++ initialization list.

See Also

abstract | override | C# Keywords | Modifiers | Versioning Tutorial


Statements

Statements are program instructions. Except as described, statements are executed in sequence. C# has the following categories of statements.

Category | C# keywords
Selection statements | if, else, switch, case
Iteration statements | do, for, foreach, in, while
Jump statements | break, continue, default, goto, return
Exception handling statements | throw, try-catch, try-finally
Checked and unchecked | checked, unchecked
Unsafe and Fixed | fixed, unsafe
lock Statement | lock

See Also

C# Keywords


Selection Statements

A selection statement causes the program control to be transferred to a specific flow based upon whether a certain condition is true or not.

The following keywords are used in selection statements:

  • if
  • else
  • switch
  • case

See Also

C# Keywords | Statements


if-else

The if statement is a control statement that executes a block of code if an expression evaluates to true. It takes the form:

if (expression)
   statement1
[else
   statement2]
where:
expression
An expression that can be implicitly converted to bool or a type that contains overloading of the true and false operators.
statement1
The embedded statement(s) to be executed if expression is true.
statement2
The embedded statement(s) to be executed if expression is false.

Remarks

If expression is true, statement1 is executed. If the optional else clause exists and expression evaluates to false, statement2 is executed. After executing the if statement, control is transferred to the next statement.

If any of the two results of the if statement (true or false) results in executing more than one statement, multiple statements can be conditionally executed by including them into blocks.

The statement(s) to be executed upon testing the condition can be of any kind, including another if statement nested into the original if statement. In nested if statements, the else clause belongs to the last if that does not have a corresponding else. For example:

if (x > 10)
   if (y > 20)
      Console.Write("Statement_1");
   else
      Console.Write("Statement_2");

In this example, Statement_2 will be displayed if the condition (y > 20) evaluates to false. However, if you want to associate Statement_2 with the condition (x >10), use braces:

if (x > 10)
{
   if (y > 20)
      Console.Write("Statement_1");
}
else
   Console.Write("Statement_2");

In this case, Statement_2 will be displayed if the condition (x > 10) evaluates to false

Example

In this example, you enter a character from the keyboard and the program checks if the input character is an alphabetic character. If so, it checks if it is lowercase or uppercase. In each case, the proper message is displayed.

// if-else example
using System;
public class IfTest
{
   public static void Main()
   {
      Console.Write("Enter a character: ");
      char c = (char) Console.Read();
      if (Char.IsLetter(c))
         if (Char.IsLower(c))
            Console.WriteLine("The character is lowercase.");
         else
            Console.WriteLine("The character is uppercase.");
      else
         Console.WriteLine("The character is not an alphabetic character.");
   }
}

Sample Runs

Run #1:

Enter a character: 2
The character is not an alphabetic character.

Run #2:

Enter a character: A
The character is uppercase.

Run #3:

Enter a character: h
The character is lowercase.

It is also possible to extend the if statement to handle multiple conditions using the following else-if arrangement:

if (Condition_1)
   Statement_1;
else if (Condition_2)
   Statement_2;
else if (Condition_3)
   Statement_3;
...
else
   Statement_n;

Example

This example checks if the input character is lowercase, uppercase, or a number. Otherwise, it is not an alphanumeric character. The program makes use of the else-if ladder.

// else-if
using System;
public class IfTest
{
   public static void Main()
   {
      Console.Write("Enter a character: ");
      char c = (char) Console.Read();

      if (Char.IsUpper(c))
         Console.WriteLine("The character is uppercase.");
      else if (Char.IsLower(c))
         Console.WriteLine("The character is lowercase.");
      else if (Char.IsDigit(c))
         Console.WriteLine("The character is a number.");
      else
         Console.WriteLine("The character is not alphanumeric.");
   }
}

Sample Runs

Run #1:

Enter a character: E
The character is uppercase.

Run #2:

Enter a character: e
The character is lowercase.

Run #3:

Enter a character: 4
The character is a number.

Run #4:

Enter a character: $
The character is not alphanumeric.

See Also

C# Keywords | Compare to C++ | switch | §21.8 Statements Grammar


switch

The switch statement is a control statement that handles multiple selections by passing control to one of the case statements within its body. The switch statement takes the form:

switch (expression)
{
 case constant-expression:
 statement
 jump-statement
 [default: statement]
}
where:
expression
An integral or string type expression.
statement
The embedded statement(s) to be executed if control is transferred to the case or the default.
jump-statement
A jump statement that transfers control out of the case body.
constant-expression
Control is transferred to a specific case according to the value of this expression.

Remarks

Control is transferred to the case statement whose constant-expression matches expression. The switch statement can include any number of case instances, but no two case constants within the same switch statement can have the same value. Execution of the statement body begins at the selected statement and proceeds until the jump-statement transfers control out of the case body. Only the last block does not require a jump-statement whether this block is a case, or a default statement.

Unlike the C++ switch statement, C# does not support an explicit fall through from one case label to another. If you want, you can use goto a switch-case, or goto default.

If expression does not match any constant-expression, control is transferred to the statement(s) that follow the optional default label. If there is no default label, control is transferred outside the switch.

Example

using System;
using System.IO;
class SwitchTest
{
   public static void Main()
   {
      Console.WriteLine("Coffee sizes: 1=Small 2=Medium 3=Large");
      Console.Write("Please enter your selection: ");
      string s = Console.ReadLine();
      int n = int.Parse(s);
      switch(n)
      {
         case 0:
            goto case 1;
         case 1:
            Console.WriteLine("You selected small size. Insert 50 cents.");
            break;
         case 2:
            Console.WriteLine("You selected medium size. Insert 75 cents.");
            break;
         case 3:
            Console.WriteLine("You selected large size. Insert $1.00.");
            break;
         default:
            Console.WriteLine("Invalid selection. Please select 1, 2, or 3.");
      }
      Console.WriteLine("Thank you for your business.");
   }
}

Sample Run

Coffee sizes: 1=Small 2=Medium 3=Large
Please enter your selection: 2
You selected medium size. Insert 75 cents.
Thank you for your business.

In the preceding example, an integral type variable, n, was used for the switch cases. Notice that you can also use the string variable, s, directly. In this case, you can use switch cases like these:

switch(s)
{
   case "1":
      ...
   case "2":
      ...
}

See Also

C# Keywords | Compare to C++ | if | §21.8 Statements Grammar


Iteration Statements

You can create loops by using the iteration statements. Iteration statements cause embedded statements to be executed a number of times, subject to the loop termination criteria. These statements are executed in order, except when a jump statement is encountered.

The following keywords are used in iteration statements:

  • do
  • for
  • foreach
  • in
  • while

See Also

C# Keywords | Statements


do

The do statement executes a statement or a block of statements repeatedly until a specified expression evaluates to false. It takes the following form:

do statement while (expression);

where:

expression
An expression that can be implicitly converted to bool or a type that contains overloading of the true and false operators. The expression is used to test the loop-termination criteria.
statement
The embedded statement(s) to be executed.

Remarks

Unlike the while statement, the body loop of the do statement is executed at least once regardless of the value of the expression.

Example

using System;
public class TestDoWhile
{
   public static void Main ()
   {
      int x;
      int y = 0;

      do
      {
         x = y++;
         Console.WriteLine(x);
      }

      while(y < 5);
   }
}

Output

0
1
2
3
4

Example

Notice in this example that although the condition evaluates to false, the loop will be executed once.

using System;
class DoTest {
   public static void Main()
   {
      int n = 10;
      do
      {
         Console.WriteLine("Current value of n is {0}", n);
         n++;
      } while (n < 6);
   }
}

Output

Current value of n is 10

See Also

C# Keywords | Compare to C++ | Iteration Statements | §21.8 Statements Grammar


for

The for loop executes a statement or a block of statements repeatedly until a specified expression evaluates to false. It takes the following form:

for ([initializers]; [expression]; [iterators]) statement

where:

initializers
A comma separated list of expressions or assignment statements to initialize the loop counters.
expression
An expression that can be implicitly converted to bool or a type that contains overloading of the true and false operators. The expression is used to test the loop-termination criteria.
iterators
Expression statement(s) to increment or decrement the loop counters.
statement
The embedded statement(s) to executed.

Remarks

The for statement executes the statement repeatedly as follows:

  • First, the initializers are evaluated.
  • Then, while the expression evaluates to true, the statement(s) are executed and the iterators are evaluated.
  • When the expression becomes false, control is transferred outside the loop.

Because the test of expression takes place before the execution of the loop, a for statement executes zero or more times.

All the expressions of the for statement are optional, for example, the following statement is used to write an infinite loop:

for (;;) {
   ...
}

Example

// for loop
using System;
public class ForLoopTest
{
   public static void Main()
   {
      for (int i = 1; i <= 5; i++)
         Console.WriteLine(i);
   }
}

Output

1
2
3
4
5

See Also

C# Keywords | Compare to C++ | Iteration Statements | §21.8 Statements Grammar


foreach, in

The foreach statement repeats a group of embedded statements for each element in an array or an object collection. The foreach statement is used to iterate through the collection to get the desired information, but should not be used to change the contents of the collection to avoid unpredictable side effects. The statement takes the following form:

foreach (type identifier in expression) statement

where:

type
The type of identifier.
identifier
The iteration variable that represents the collection element.
expression
Object collection or array expression. The type of the collection element must be convertible to the identifier type.
statement
The embedded statement(s) to be executed.

Remarks

The embedded statements continue to execute for each element in the array or collection. After the iteration has been completed for all the elements in the collection, control is transferred to the next statement following the foreach block.

In this section, the following topics are discussed in detail:

  • Using foreach with Arrays
  • Using foreach with Collections

Using foreach with Arrays

When used with an array, the foreach statement repeats the embedded statement(s) for each element in the array.

Example

In this example, an array of integers is searched for even and odd numbers. A counter for each type of number stores the number of occurrences.

// Using foreach with arrays
using System;
class MainClass
{
   public static void Main()
   {
      int odd = 0, even = 0;
      int[] arr = new int [] {0,1,2,5,7,8,11};

      foreach (int i in arr)
      {
         if (i%2 == 0)
            even++;
         else
            odd++;
      }

      Console.WriteLine("Found {0} Odd Numbers, and {1} Even Numbers.",
                        odd, even) ;
   }
}

Output

Found 4 Odd Numbers, and 3 Even Numbers.

Using foreach with Collections

To iterate through a collection, the collection must meet specific requirements. For example, in the following foreach statement:

   foreach (ItemType item in myCollection)

myCollection must meet the following requirements:

  • The collection type:
    • Must be one of the types: interface, class, or struct.
    • Must include an instance method named GetEnumerator that returns a type, for example, Enumerator (explained below).
  • The type Enumerator (a class or struct) must contain:
    • A property named Current that returns ItemType or a type that can be converted to it. The property accessor returns the current element of the collection.
    • A bool method, named MoveNext, that increments the item counter and returns true if there are more items in the collection.

There are three approaches to using collections:

  1. Create a collection using the above instructions. This collection can only be used in a C# program.
  2. Create a generic collection using the above instructions, in addition to implementing the IEnumerable interface. This collection can be used from other languages such as Visual Basic.
  3. Use one of the predefined collections in the collection classes.

The following examples demonstrate the three approaches.

Example | Demonstrates | Comment
Example 1 | A C#-specific collection. | This example creates a collection by using the instructions above.
Example 2 | A generic collection. | This example creates a collection by using the instructions above and implements the IEnumerable and IEnumerator interfaces.
Example 3 | Using one of the predefined collection classes. | This example creates a Hashtable instance and uses the Hashtable class members to manipulate the collection. The Hashtable class represents a dictionary of associated keys and values, implemented as a hash table.

For more information on the interfaces IEnumerator and IEnumerable, and the Hashtable class, see the System.Collections section in the .NET SDK.

Example 1

// Using foreach with C#-specific collections:
using System;

// Declare the collection:
public class MyCollection
{
   int[] items;

   public MyCollection()
   {
      items = new int[5] {12, 44, 33, 2, 50};
   }

   public MyEnumerator GetEnumerator()
   {
      return new MyEnumerator(this);
   }

   // Declare the enumerator class:
   public class MyEnumerator
   {
      int nIndex;
      MyCollection collection;
      public MyEnumerator(MyCollection coll)
      {
         collection = coll;
         nIndex = -1;
      }

      public bool MoveNext()
      {
         nIndex++;
         return(nIndex < collection.items.GetLength(0));
      }

      public int Current
      {
         get
         {
            return(collection.items[nIndex]);
         }
      }
   }
}

public class MainClass
{
   public static void Main()
   {
      MyCollection col = new MyCollection();
      Console.WriteLine("Values in the collection are:");

      // Display collection items:
      foreach (int i in col)
      {
         Console.WriteLine(i);
      }
   }
}

Output

Values in the collection are:
12
44
33
2
50

Example 2

This example repeats the same algorithm of Example 1, but uses a generic collection that can be enumerated from other languages such as Visual Basic. This type of collection must implement the IEnumerable interface from the System.Collections namespace.

// Using foreach with a generic collection
using System;
using System.Collections;

// Declare the collection and implement the IEnumerable interface:
public class MyCollection: IEnumerable
{
   int[] items;
   public MyCollection()
   {
      items = new int[5] {12, 44, 33, 2, 50};
   }

   public MyEnumerator GetEnumerator()
   {
      return new MyEnumerator(this);
   }

   // Implement the GetEnumerator() method:
   IEnumerator IEnumerable.GetEnumerator()
   {
      return GetEnumerator();
   }

   // Declare the enumerator and implement the IEnumerator interface:
   public class MyEnumerator: IEnumerator
   {
      int nIndex;
      MyCollection collection;
      public MyEnumerator(MyCollection coll)
      {
         collection = coll;
         nIndex = -1;
      }

      public void Reset()
      {
         nIndex = -1;
      }

      public bool MoveNext() {
         nIndex++;
         return(nIndex < collection.items.GetLength(0));
      }

      public int Current
      {
         get
         {
            return(collection.items[nIndex]);
         }
      }

      // The current property on the IEnumerator interface:
      object IEnumerator.Current
      {
         get
         {
            return(Current);
         }
      }
   }
}

public class MainClass
{
   public static void Main(string [] args)
   {
      MyCollection col = new MyCollection();
      Console.WriteLine("Values in the collection are:");

      // Display collection items:
      foreach (int i in col)
      {
         Console.WriteLine(i);
      }
   }
}

Output

Values in the collection are:
12
44
33
2
50

Example 3

In this example, the predefined Hashtable collection class is used. By using the System.Collections namespace in your program, you can have access to the Hashtable class and its members. To add entries to the Hashtable object, use the Add method.

// Using the Hashtable collection class
using System;
using System.Collections;
public class MainClass
{
   public static void Main(string [] args)
   {
      // Declare a Hashtable object:
      Hashtable ziphash = new Hashtable();

      // Add entries using the Add() method:
      ziphash.Add("98008", "Bellevue");
      ziphash.Add("98052", "Redmond");
      ziphash.Add("98201", "Everett");
      ziphash.Add("98101", "Seattle");
      ziphash.Add("98371", "Puyallup");

      // Display contents:
      Console.WriteLine("Zip code\tCity");
      foreach (string zip in ziphash.Keys)
      {
         Console.WriteLine(zip + "\t\t" + ziphash[zip]);
      }
   }
}

Output

Zip code        City
98201           Everett
98052           Redmond
98101           Seattle
98008           Bellevue
98371           Puyallup

See Also

C# Keywords | Iteration Statements | §21.8 Statements Grammar | Arrays | Collection Classes Tutorial


while

The while statement executes a statement or a block of statements until a specified expression evaluates to false. It takes the form:

while (expression) statement

where:

expression
An expression that can be implicitly converted to bool or a type that contains overloading of the true and false operators. The expression is used to test the loop-termination criteria.
statement
The embedded statement(s) to be executed.

Remarks

Because the test of expression takes place before each execution of the loop, a while loop executes zero or more times.

A while loop can be terminated when a break, goto, return, or throw statement transfers control outside the loop. To pass control to the next iteration without exiting the loop, use the continue statement.

Example

using System;
class WhileTest
{
   public static void Main()
   {
      int n = 1;

      while (n < 6)
      {
         Console.WriteLine("Current value of n is {0}", n);
         n++;
      }
   }
}

Output

Current value of n is 1
Current value of n is 2
Current value of n is 3
Current value of n is 4
Current value of n is 5

See Also

C# Keywords | Compare to C++ | Iteration Statements | §21.8 Statements Grammar


Jump Statements

Branching is performed using jump statements, which cause an immediate transfer of the program control. The following keywords are used in jump statements:

  • break
  • continue
  • default
  • goto
  • return

See Also

C# Keywords | Statements


break

The break statement terminates the closest enclosing loop or conditional statement in which it appears. Control is passed to the statement that follows the terminated statement, if any. The break statement takes the form:

break;

Example

In this example, the conditional statement contains a counter that is supposed to count from 1 to 100; however, the break statement terminates the loop after 4 counts.

using System;
class BreakTest
{
   public static void Main()
   {
      for (int i = 1; i <= 100; i++)
      {
         if (i == 5)
            break;
         Console.WriteLine(i);
      }
   }
}

Output

1
2
3
4

Example

This example demonstrates the use of break in a switch statement.

// break and switch
using System;
using System.IO;

class Switch
{
   public static void Main()
   {
      Console.Write("Enter your selection (1, 2, or 3): ");
      string s = Console.ReadLine();
      int n = Int32.FromString(s);

      switch(n)
      {
         case 1:
            Console.WriteLine("Current value is {0}", 1);
            break;
         case 2:
            Console.WriteLine("Current value is {0}", 2);
            break;
         case 3:
            Console.WriteLine("Current value is {0}", 3);
            break;
         default:
            Console.WriteLine("Sorry, invalid selection.");
      }
   }
}

Sample Runs

Run #1:

Enter your selection (1, 2, or 3): 2
Current value is 2

Run #2:

Enter your selection (1, 2, or 3): 4
Sorry, invalid selection.

See Also

C# Keywords | Compare to C++ | switch | Jump Statements | §21.8 Statements Grammar


continue

The continue statement passes control to the next iteration of the enclosing iteration statement in which it appears. It takes the form:

continue;

Example

In this example, a counter is initialized to count from 1 to 10. By using the continue statement in conjunction with the expression (i < 9), the statements between continue and the end of the for body are skipped.

using System;
class ContinueTest
{
   public static void Main()
   {
      for (int i = 1; i <= 10; i++)
      {
         if (i < 9)
            continue;
         Console.WriteLine(i);
      }
   }
}

Output

9
10

See Also

C# Keywords | Compare to C++ | Jump Statements | §21.8 Statements Grammar


goto

The goto statement transfers the program control directly to a labeled statement. It takes one of the following forms:

goto identifier;
goto case constant-expression;
goto default;

where:

identifier
A label.
constant-expression
A switch-case label.

Remarks

In the first form, the identifier indicates a label located in the current body, the same lexical scope, or an enclosing scope of the goto statement.

A common use of goto is to transfer control to a specific switch-case label or the default label in a switch statement.

The goto statement is also useful to get out of deeply nested loops.

A warning message may be issued if the label has never been referenced in the program. For more information on labels, see §3.1 Declarations.

Example

For an example of using goto to transfer control to a specific switch-case label, see the switch example.

Example

The following example demonstrates using goto to break out from nested loops.

// Nested search loops
using System;
using System.IO;

public class GotoTest1
{
   public static void Main()
   {
      int x = 200, y = 4;
      int[,] myArray = new int[x,y];

      // initialize the array:
      for (int i = 0; i < x; i++)
         for (int j = 0; j < y; j++)
            myArray[i,j] = ++i;

      // Read input:
      Console.Write("Enter the number to search for: ");

      // input a char
      string s = Console.ReadLine();
      int myNumber = Int32.FromString(s);

      // Search:
      for (int i = 0; i < x; i++)
         for (int j = 0; j < y; j++)
            if (myArray[i,j] == myNumber)
               goto Found;

      Console.WriteLine("The number {0} was not found.", myNumber);
      goto Finish;

   Found:
      Console.WriteLine("The number {0} is found.", myNumber);

   Finish:
      Console.WriteLine("End of search.");
   }
}

Sample Run

Enter the number to search for: 44
The number 44 is found.
End of search.

Example

// Labels outside the scope
using System;

class UnreachableCode
{
   public static void Main()
   {
      int x = 55;
      Console.WriteLine("x = {0}", x);

      if (x == 55)
      {
         x = 135;
         goto A;   // Error
      }

      x = x + 1;

      for (int i=1; i<=5; i++)
      {
         A: Console.WriteLine(i);
      }

      Console.WriteLine("x = {0}", x);
   }
}

In the preceding example, the goto statement is referencing a label A outside its scope. The compiler will issue the error message 0159:

No such label 'A' within the scope of the goto statement

A warning message may also be issued because the label has never been referenced.

If you move the label A to the beginning of the for loop, the program would compile and run normally, that is:

A:      for (int i=1; i<=5; i++) {   // Now the program compiles.

See Also

C# Keywords | Compare to C++ | Jump Statements | §21.8 Statements Grammar


return

The return statement terminates execution of the method in which it appears and returns control to the calling method. It can also return the value of the optional expression. If the method is of the type void, the return statement can be omitted. The return statement takes the form:

return [expression];

where:

expression
The value returned by a method. The expression is not used with methods of the type void.

Example

In the following example, the method A() returns the variable Area as a double value.

using System;

class ReturnTest

{

   static double CalculateArea(int r)

   {

      double area;

      area = r*r*Math.PI;

      return area;

   }

   public static void Main()

   {

      int radius = 5;

      Console.WriteLine("The area is {0:0.00}", CalculateArea(radius));

   }

}

Output

The area is 78.54

See Also

C# Keywords | Compare to C++ | Jump Statements | §21.8 Statements Grammar


Exception Handling Statements

C# provides built-in support for handling anomalous situations, known as exceptions, which may occur during the execution of your program. These exceptions are handled by code that is outside the normal flow of control. The try, throw, catch, and finally keywords implement exception handling.

The following exception handling topics are explained in this section:

  • throw
  • try-catch
  • try-finally
  • try-catch-finally

See Also

C# Keywords | Statements


throw

The throw statement is used to signal the occurrence of an anomalous situation (exception) during the program execution. The throw statement takes the form:

throw [expression];

where:

expression
The exception object. This is omitted when re-throwing the current exception object in a catch clause.

Remarks

The thrown exception is an object whose class is derived from System.Exception, for example:

class MyException : System.Exception {}
throw new MyException();

Usually the throw statement is used with try-catch or try-finally statements. When an exception is thrown, the program looks for the catch statement that handles this exception.

Example

This example demonstrates how to throw an exception using the throw statement.

// throw example
using System;
public class ThrowTest
{
   public static void Main()
   {
      string s = null;

      if (s == null)
      {
         throw(new ArgumentNullException());
      }

      Console.Write("The string s is null"); // not executed
   }
}

Output

Caught exception: System.ArgumentNullException: argument can't be null at ThrowTest.Main()

Example

See the try-catch, try-finally and try-catch-finally examples.

See Also

Compare to C++ | C# Keywords | Exception Handling Statements | §21.8 Statements Grammar


try-catch

The try-catch statement consists of a try block followed by one or more catch clauses, which specify handlers for different exceptions. The try-catch statement takes one of the forms:

try try-block catch (exception-declaration-1) catch-block-1 catch (exception-declaration-2) catch-block-2 ...
try try-block catch catch-block

where:

try-block
Contains the code segment expected to raise the exception.
exception-declaration
The exception object declaration.
catch-block
Contains the exception handler.

Remarks

The try-block contains the guarded code block that may cause the exception. The block is executed until an exception is thrown or it is completed successfully. For example, the following attempt to cast a null object raises the NullReferenceException exception:

object o2 = null;
try
{
   int i2 = (int) o2;   // Error
}

The catch clause can be used without arguments, in which case it catches any type of exception, and referred to as the general catch clause. It can also take an object argument derived from System.Exception, in which case it handles a specific exception. For example:

catch (InvalidCastException e)
{
}

It is possible to use more than one specific catch clause in the same try-catch statement. In this case, the order of the catch clauses is important because the catch clauses are examined in order. Catch the more specific exceptions before the less specific ones.

A throw statement can be used in the catch block to rethrow the exception, which has been caught by the catch statement. For example:

catch (InvalidCastException e)
{
throw (e);   // Rethrowing exception e
}

If you want to rethrow the exception currently handled by a parameterless catch clause, use the throw statement without arguments. For example:

catch
{
throw;
}

For more information on catch, see try-catch-finally.

Example

In this example, the try block contains a call to the method MyFn() that may cause an exception. The catch clause contains the exception handler that simply displays a message on the screen. When the throw statement is called from inside MyFn(), the system looks for the catch statement and displays the message Exception caught.

// Rethrowing exceptions:
using System;
class MyClass
{
   public static void Main()
   {
      MyClass x = new MyClass();
      try
      {
         string s = null;
         x.MyFn(s);
      }

      catch (Exception e)
      {
         Console.WriteLine("{0} Exception caught.", e);
      }
   }

   public void MyFn(string s)
   {
      if (s == null)
         throw(new ArgumentNullException());
   }
}

Output

System.ArgumentNullException: argument can't be null
   at MyClass.Main() Exception caught.

Example

In this example, two catch statements are used. The most specific exception, which comes first, is caught.

// Ordering catch clauses
using System;
class MyClass
{
   public static void Main()
   {
      MyClass x = new MyClass();
      try
      {
         string s = null;
         x.MyFn(s);
      }

      // Most specific:
      catch (ArgumentNullException e)
      {
         Console.WriteLine("{0} First exception caught.", e);
      }

      // Least specific:
      catch (Exception e)
      {
         Console.WriteLine("{0} Second exception caught.", e);
      }

   }

   public void MyFn(string s)
   {
      if (s == null)
         throw new ArgumentNullException();
   }
}

Output

System.ArgumentNullException: argument can't be null
   at MyClass.Main() First exception caught.

In the preceding example, if you start with the least specific catch clause, you will get the error message:

A previous catch clause already catches all exceptions of this or a super type ('System.Exception')

However, to catch the least specific exception, replace the throw statement by the following one:

throw new Exception();

See Also

C# Keywords | Compare to C++ | Exception Handling Statements | throw | try-finally | §21.8 Statements Grammar


try-finally

The finally block is useful for cleaning up any resources allocated in the try block. Control is always passed to the finally block regardless of how the try block exits. The try-finally statement takes the form:

try try-block finally finally-block

where:

try-block
Contains the code segment expected to raise the exception.
finally-block
Contains the exception handler and the cleanup code.

Remarks

Whereas catch is used to handle exceptions that occur in a statement block, finally is used to guarantee a statement block of code executes regardless of how the preceding try block is exited.

Example

In this example, there is one illegal conversion statement that causes an exception. When you run the program, you get a run-time error message, but the finally clause will still be executed and display the output.

// try-finally
using System;
public class TestTryFinally
{
   public static void Main()
   {
      int i = 123;
      string s = "Some string";
      object o = s;

      try
      {
         // Illegal conversion; o contains a string not an int
         i = (int) o;
      }

      finally
      {
         Console.Write("i = {0}", i);
      }
   }
}

Output

i = 123

Caught exception: System.InvalidCastException

   at TestTryFinally.Main()

Although an exception was caught, the output statement included in the finally block was executed.

For more information on finally, see try-catch-finally.

See Also

C# Keywords | Compare to C++ | Exception Handling Statements | throw | try-catch | §21.8 Statements Grammar


try-catch-finally

A common usage of catch and finally together is to obtain and use resources in a try block, deal with exceptional circumstances in a catch block, and release the resources in the finally block.

Example

// try-catch-finally
using System;
public class EHClass
{
   public static void Main ()
   {
      try
      {
         Console.WriteLine("Executing the try statement.");
         throw new NullReferenceException();
      }

      catch(NullReferenceException e)
      {
         Console.WriteLine("{0} Caught exception #1.", e);
      }

      catch
      {
         Console.WriteLine("Caught exception #2.");
      }

      finally
      {
         Console.WriteLine("Executing finally block.");
      }
   }
}

Output

Executing the try statement.
System.NullReferenceException
   at EHClass.Main() Caught exception #1.
Executing finally block.

See Also

C# Keywords | Compare to C++ | Exception Handling Statements | throw | §21.8 Statements Grammar


Checked and Unchecked

C# statements can execute in either checked or unchecked context. In a checked context, arithmetic overflow raises an exception. In an unchecked context, arithmetic overflow is ignored and the result is truncated.

  • checked Specify checked context.
  • unchecked Specify unchecked context.

If neither checked nor unchecked is specified, the default context depends on external factors such as compiler options.

The following operations are affected by the overflow checking:

  • Expressions using the following predefined operators on integral types:

    ++ — - (unary) + - * /

  • Explicit numeric conversions between integral types.

The /checked compiler option lets you specify checked or unchecked context for all integer arithmetic statements that are not explicitly in the scope of a checked or unchecked keyword.

See Also

C# Keywords | Statements


checked

The checked keyword is used to control the overflow-checking context for integral-type arithmetic operations and conversions. It can be used as an operator or a statement according to the following forms.

The checked statement:

checked block

The checked operator:

checked (expression)

where:

block
The statement block that contains the expressions to be evaluated in a checked context.
expression
The expression to be evaluated in a checked context. Notice that the expression must be in parentheses ( ).

Remarks

In a checked context, if an expression produces a value that is outside the range of the destination type, the result depends on whether the expression is constant or non-constant. Constant expressions cause compile time errors, while non-constant expressions are evaluated at run time and raise exceptions.

If neither checked nor unchecked is used, a constant expression uses the default overflow checking at compile time, which is checked. Otherwise, if the expression is non-constant, the run-time overflow checking depends on other factors such as compiler options and environment configuration.

The following three examples demonstrate the checked and unchecked operators on non-constant expressions. All use the same algorithm, but different checking contexts. The overflow checking is evaluated at run time.

  • Example 1: Using a checked expression.
  • Example 2: Using an unchecked expression.
  • Example 3: Using default overflow checking.

Only the first example throws an overflow exception at run time, in which case, you have the option to go to the debugging mode, or abort the program execution. The other two examples produce truncated values.

See also the unchecked examples on using the checked and unchecked statements.

Example 1

// Using checked expressions
// The overflow of non-constant expressions is checked at run time
using System;

class OverFlowTest
{
   static short x = 32767;   // Max short value
   static short y = 32767;

   // Using a checked expression
   public static int myMethodCh()
   {
      int z =  checked((short)(x + y));
      return z;   // Throws the exception OverflowException
   }

   public static void Main()
   {
      Console.WriteLine("Checked output value is: {0}", myMethodCh());
   }
}

Output

When you run the program, it throws the exception OverflowException. You can debug the program or abort execution.

Example 2

// Using unchecked expressions
// The overflow of non-constant expressions is checked at run time
using System;

class OverFlowTest
{
   static short x = 32767;   // Max short value
   static short y = 32767;

   public static int myMethodUnch()
   {
      int z =  unchecked((short)(x + y));
      return z;   // Returns -2
   }

   public static void Main()
   {
      Console.WriteLine("Unchecked value is: {0}", myMethodUnch());
   }
}

Output

Unchecked value is: -2

Example 3

// Using default overflow checking
// The overflow of non-constant expressions is checked at run time
using System;

class OverFlowTest
{
   static short x = 32767;   // Max short value
   static short y = 32767;

   public static int myMethodUnch()
   {
      int z =  (short)(x + y);
      return z;   // Returns -2
   }

   public static void Main()
   {
      Console.WriteLine("Default checking ouput value is: {0}", myMethodUnch());
   }
}

Output

Default checking ouput value is: -2

See Also

C# Keywords | Checked and Unchecked | unchecked


unchecked

The unchecked keyword is used to control the overflow-checking context for integral-type arithmetic operations and conversions. It can be used as an operator or a statement according to the following forms.

The unchecked statement:

unchecked block

The unchecked operator:

unchecked (expression)

where:

block
The statement block that contains the expressions to be evaluated in an unchecked context.
expression
The expression to be evaluated in an unchecked context. Notice that the expression must be in parentheses ( ).

Remarks

In an unchecked context, if an expression produces a value that is outside the range of the destination type, the result is truncated.

If neither checked nor unchecked is used, a constant expression uses the default overflow checking at compile time, which is checked. Otherwise, if the expression is non-constant, the run-time overflow checking depends on other factors such as compiler options and environment configuration.

The following are three examples demonstrating the checked and unchecked statements. All use the same algorithm, but different checking contexts. The three examples are using constant expressions, for which the overflow checking is evaluated at compile time.

  • Example 1: Using the unchecked statement.
  • Example 2: Using default overflow checking.
  • Example 3: Using the checked statement.

Only the unchecked statement produces a truncated value. The other two statements produce compile-time errors.

See also the checked examples on using the checked and unchecked operators.

Example 1

// Using the unchecked statement with constant expressions
// Overflow is checked at compile time
using System;

class TestClass
{
   const int x = 2147483647;   // Max int
   const int y = 2;

   public int MethodUnCh()
   {
      // Unchecked statement:
      unchecked
      {
         int z = x * y;
         return z;   // Returns -2
      }
   }

   public static void Main()
   {
      TestClass myObject = new TestClass();
      Console.WriteLine("Unchecked output value: {0}",
         myObject.MethodUnCh());
   }
}

Output

Unchecked output value: -2

Example 2

// Using default overflow checking with constant expressions
// Overflow is checked at compile time
using System;

class TestClass
{
   const int x = 2147483647;   // Max int
   const int y = 2;

   public int MethodDef()
   {
      // Using default overflow checking:
      int z = x * y;
      return z;   // Compiler error
   }

   public static void Main()
   {
      TestClass myObject = new TestClass();
      Console.WriteLine("Default checking value: {0}",
         myObject.MethodDef());
   }
}

Output

The compilation error: The operation overflows at compile time in checked mode.

Example 3

// Using the checked statement with constant expressions
// Overflow is checked at compile time
using System;

class TestClass
{
   const int x = 2147483647;   // Max int
   const int y = 2;

   public int MethodCh()
   {
      // Checked statement:
      checked
      {
         int z = (x * y);
         return z;   // Compiler error
      }
   }

   public static void Main()
   {
      TestClass myObject = new TestClass();
      Console.WriteLine("Checked value: {0}", myObject.MethodCh());
   }
}

Output

The compilation error: The operation overflows at compile time in checked mode.

See Also

C# Keywords | Checked and Unchecked | checked


Unsafe and Fixed

C# statements execute in either a safe or an unsafe context. Safe context is the default, but any code using pointers requires unsafe context.

  • unsafe Specifies unsafe context.

Once you have a pointer to a variable, you must ensure that the variable isn't moved in memory by the garbage collector. In C# you can "pin" a variable using the fixed statement.

  • fixed Prevents relocation of a variable.

See Also

C# Keywords | Statements


fixed

Prevents relocation of a variable by the garbage collector.

fixed ( type* ptr = expr ) statement

where:

type
An unmanaged type or void.
ptr
A pointer name.
expr
An expression that is implicitly convertible to type*.
statement
Executable statement or block.

Remarks

The fixed statement is only permitted in an unsafe context.

The fixed statement sets a pointer to a managed variable and "pins" that variable during the execution of statement. Without fixed, pointers to managed variables would be of little use since garbage collection could relocate the variables unpredictably. (In fact, the C# compiler will not allow you to set a pointer to a managed variable except in a fixed statement.)

// assume class Point { public int x, y; }
Point pt = new Point();    // pt is a managed variable, subject to g.c.
fixed ( int* p = &pt.x ){  // must use fixed to get address of pt.x and
    *p = 1;                //   pin pt in place while we use the pointer
}

You can initialize a pointer with the address of an array or a string:

fixed (int* p = arr) ...  // equivalent to p = &arr[0]
fixed (char* p = str) ... // equivalent to p = &str[0]

You can initialize multiple pointers, as long as they are all of the same type:

fixed (byte* ps = srcarray, pd = dstarray) {...}

To initialize pointers of different type, simply nest fixed statements:

fixed (int* p1 = &p.x)
   fixed (double* p2 = &array[5])
      // do something with p1 and p2

Pointers initialized in fixed statements cannot be modified.

After statement is executed, any pinned variables are unpinned and subject to garbage collection. Therefore, do not point to those variables outside the fixed statement.

In unsafe mode, you can allocate memory on the stack, where it is not subject to garbage collection and therefore does not need to be pinned. See stackalloc for more information.

Example

using System;

class Point {
   public int x, y;
}

class FixedTest
{
   // unsafe method: takes pointer to int
   unsafe static void SquarePtrParam (int* p)
   {
      *p *= *p;
   }

   public static void Main()
   {
      Point pt = new Point();
      pt.x = 5;
      pt.y = 6;

      unsafe
      {
         // pin pt in place:
         fixed (int* p = &pt.x)
         {
            SquarePtrParam (p);
         }
         // pt now unpinned
      }

      Console.WriteLine ("{0} {1}", pt.x, pt.y);
   }
}

Output

25 6

See Also

C# Keywords | Unsafe and Fixed | Unmanaged code Tutorial


unsafe

The unsafe keyword denotes an unsafe context. Unsafe context is required for any operation involving pointers.

unsafe can be applied as a modifier in the declaration of callable members such as methods, properties, constructors, and extenders (but not static constructors).

static unsafe void FastCopy ( byte[] src, byte[] dst, int count )
{
   // unsafe context: can use pointers here
}

The scope of the unsafe context extends from the parameter list to the end of the function, so pointers can also be used in the parameter list:

static unsafe void FastCopy ( byte* ps, byte* pd, int count ) {...}

Alternatively, unsafe can be used to specify an unsafe statement — a statement or block that is an unsafe context:

unsafe *p++ = *q++;    // unsafe assignment statement
unsafe { *p++ = 1; *q++ = 2; }  // unsafe block
unsafe fixed (byte* p = src, q = dst) {...}  // unsafe fixed statement

To compile unmanaged code you must specify the /unsafe compiler option. Unmanaged code is not verifiable by the Common Language Runtime.

Example

using System;
class UnsafeTest
{
   // unsafe method: takes pointer to int
   unsafe static void SquarePtrParam (int* p)
   {
      *p *= *p;
   }

   public static void Main()
   {
      int i = 5;
      // unsafe statement for address-of operator
      unsafe SquarePtrParam (&i);
      Console.WriteLine (i);
   }
}

Output

25

See Also

C# Keywords | Unsafe and Fixed | Unmanaged code Tutorial


lock Statement

The lock keyword marks a statement block as a critical section.

lock(expression) statement_block

where:

expression
Specifies the object that you want to lock on. expression must be a reference type.

Typically, expression will either be this, if you want to protect an instance variable, or typeof (class), if you want to protect a static variable (or if the critical section occurs in a static method in the given class).

statement_block
The statements of the critical section.

Remarks

lock ensures that one thread does not enter a critical section while another thread is in the critical section of code.

§8.12 The lock statement discusses lock.

Example

The following sample shows a simple use of threads in C#.

using System;
using System.Threading;

class ThreadTest
{
   public void runme()
   {
      Console.WriteLine("runme called");
   }

   public static void Main()
   {
      ThreadTest b = new ThreadTest();
      Thread t = new Thread(new ThreadStart(b.runme));
      t.Start();
   }
}

Program Output

runme called

Example

The following sample uses threads and lock. As long as the lock statement is present, the statement block is a critical section and balance will never become a negative number.

using System;
using System.Threading;

internal class Account
{
   int balance;

   Random r = new Random();

   internal Account(int initial)
   {
      balance = initial;
   }

   internal int Withdraw(int amount)
   {

      if (balance < 0)
      {
         throw new Exception("Negative Balance");
      }

      // Comment out the next line to see the effect of leaving out the lock keyword
      lock (this)
      {
         if (balance >= amount)
         {
            Thread.Sleep(5);
            balance = balance - amount;
            return amount;
         }

         else
         {
            return 0; // transaction rejected
         }
      }
   }

   internal void DoTransactions()
   {
      for (int i = 0; i < 100; i++)
      {
         Withdraw(r.Next(-50, 100));
      }
   }
}

internal class Test
{

   static internal Thread[] threads = new Thread[10];

   public static void Main()
   {
      Account acc = new Account (0);

      for (int i = 0; i < 10; i++)
      {
         Thread t = new Thread(new ThreadStart(acc.DoTransactions));
         threads[i] = t;
      }

      for (int i = 0; i < 10; i++)
      {
         threads[i].Start();
      }
   }
}

See Also

C# Keywords | Statements | Threading Tutorial


Method Parameters

If a parameter is declared for a method without ref or out, the parameter can have a value associated with it. That value can be changed in the method, but the changed value will not be retained when control passes back to the calling procedure. By using a method parameter keyword, you can change this behavior.

This section describes the keywords you can use when declaring method parameters:

  • params
  • ref
  • out

See Also

C# Keywords


params

The params keyword lets you specify a method parameter that takes an argument where the number of arguments is variable.

No additional parameters are permitted after the params keyword in a method declaration, and only one params keyword is permitted in a method declaration.

Example

using System;
public class MyClass
{

   public static void UseParams(params int[] list)
   {
      for ( int i = 0 ; i < list.Length ; i++ )
         Console.WriteLine(list[i]);
      Console.WriteLine();
   }

   public static void UseParams2(params object[] list)
   {
      for ( int i = 0 ; i < list.Length ; i++ )
         Console.WriteLine((object)list[i]);
      Console.WriteLine();
   }

   public static void Main()
   {
      UseParams(1, 2, 3);
      UseParams2(1, 'a', "test");

      int[] myarray = new int[3] {10,11,12};
      UseParams(myarray);
   }
}

Output

1
2
3

1
a
test

10
11
12

See Also

C# Keywords


ref

The ref method parameter keyword on a method parameter causes a method to refer to the same variable that was passed into the method. Any changes made to the parameter in the method will be reflected in that variable when control passes back to the calling method.

To use a ref parameter, the argument must explicitly be passed to the method as a ref argument. The value of a ref argument will be passed to the ref parameter.

An argument passed to a ref parameter must first be initialized. Compare this to an out parameter, whose argument does not have to be explicitly initialized before being passed to an out parameter.

An overload will occur if declarations of two methods differ only in their use of ref.

A property is not a variable and cannot be passed as a ref parameter.

For information on passing an array, see Passing Arrays Using ref and out.

Example

using System;
public class MyClass
{
   public static void TestRef(ref char i)
   {
      // The value of i will be changed in the calling method
      i = 'b';
   }

   public static void TestNoRef(char i)
   {
      // The value of i will be unchanged in the calling method
      i = 'c';
   }

   // This function passes a variable as a ref parameter; the value of the
   // variable is changed after control passes back to this function.
   // The same variable is passed as a value parameter; the value of the
   // variable is unchanged after control is passed back to this function.
   public static void Main()
   {

      char i = 'a';    // variable must be initialized
      TestRef(ref i);  // the arg must be passed as ref
      Console.WriteLine(i);
      TestNoRef(i);
      Console.WriteLine(i);
   }
}

Output

b
b

See Also

C# Keywords


out

The out method parameter keyword on a method parameter causes a method to refer to the same variable that was passed into the method. Any changes made to the parameter in the method will be reflected in that variable when control passes back to the calling method.

Declaring an out method is useful when you want a method to return multiple values. A method that uses an out parameter can still return a value. A method can have more than one out parameter.

To use an out parameter, the argument must explicitly be passed to the method as an out argument. The value of an out argument will not be passed to the out parameter.

A variable passed as an out argument need not be initialized. However, the out parameter must be assigned a value before the method returns.

An overload will occur if declarations of two methods differ only in their use of out.

A property is not a variable and cannot be passed as an out parameter.

For information on passing an array, see Passing Arrays Using ref and out.

Example

using System;
public class MyClass
{
   public static int TestOut(out char i)
   {
      i = 'b';
      return -1;
   }

   public static void Main()
   {
      char i;   // variable need not be initialized
      Console.WriteLine(TestOut(out i));
      Console.WriteLine(i);
   }
}

Output

-1
b

See Also

C# Keywords


Namespaces

This section describes the keywords that are associated with using namespaces:

  • namespace
  • using

See Also

C# Keywords


namespace

The namespace keyword is used to declare a scope. This namespace scope lets you organize code and gives you a way to create globally-unique types.

namespace name[.name1] ...] {
type declarations
}

where:

name, name1
A namespace name can be any legal identifier. A namespace name can contain periods.
type declarations
Within a namespace, you can declare one or more of the following types:
  • another namespace
  • class
  • interface
  • struct
  • enum
  • delegate

Remarks

Even if you do not explicitly declare one, a default namespace is created. This unnamed namespace, sometimes called the global namespace, is present in every file. Any identifier in the global namespace is available for use in a named namespace.

Namespaces implicitly have public access and this is not modifiable.

§3.6 Namespace and type names and §9. Namespaces discuss namespaces in more detail.

See §7.3 Member lookup for information about how the compiler resolves names.

See Access Modifiers for a discussion on the access modifiers that you can assign to elements within a namespace.

It is possible to define a namespace in two or more declarations. For example, the following sample defines both classes as part of namespace MyCompany:

namespace MyCompany.Proj1
{
   class MyClass
{
   }
}

namespace MyCompany.Proj1
{
   class MyClass1
   {
   }
}

Example

using System;
namespace SomeNameSpace
{
   public class MyClass
   {
      public static void Main()
      {
         Nested.NestedNameSpaceClass.SayHello();
      }
   }

   namespace Nested   // a nested namespace
   {
      public class NestedNameSpaceClass
      {
         public static void SayHello()
         {
            Console.WriteLine("Hello");
         }
      }
   }
}

Output

Hello

See Also

C# Keywords


using

The using keyword has two uses:

  • Create an alias for a namespace (a using alias).
  • Permit the use of types in a namespace, such that, you do not have to qualify the use of a type in that namespace (a using directive).
using [alias = ]class_or_namespace;

where:

alias
A user-defined symbol that you want to represent a namespace. You will then be able to use alias to represent the namespace name.
class_or_namespace
The namespace name that you want to either use or alias, or the class name that you want to alias.

Remarks

You would create a using alias to make it easier to qualify an identifier to a namespace or class.

You would create a using directive to use the types in a namespace without having to specify the namespace. A using directive does not give you access to any namespaces that may be nested in the namespace you specify.

Namespace come in two categories: user-defined and system-defined. User-defined namespaces are namespaces defined in your code. See the documentation for the .NET Framework for a list of the system-defined namespaces.

Example

The following sample shows how to define and use a using alias for a namespace:

using MyAlias = MyCompany.Proj.Nested;   // define an alias to represent a namespace

namespace MyCompany.Proj
{
   public class MyClass
   {
      public static void DoNothing()
      {
      }
   }

   namespace Nested   // a nested namespace
   {
      public class ClassInNestedNameSpace
      {
         public static void SayHello()
         {
            System.Console.WriteLine("Hello");
         }
      }
   }

}

public class UnNestedClass
{
   public static void Main()
   {
      MyAlias.ClassInNestedNameSpace.SayHello();   // using alias
   }
}

Output

Hello

Example

The following sample shows how to define a using directive and a using alias for a class:

using System;   // using directive
using AliasToMyClass = NameSpace1.MyClass;   // using alias for a class

namespace NameSpace1
{
   public class MyClass
   {
      public override string ToString()
      {
         return "You are in NameSpace1.MyClass";
      }
   }
}

namespace NameSpace2
{
   class MyClass
   {
   }
}

namespace NameSpace3
{
   using NameSpace1;   // using directive
   using NameSpace2;   // using directive

   class Test
   {
      public static void Main()
      {
         AliasToMyClass somevar = new AliasToMyClass();
         Console.WriteLine(somevar);
      }
   }
}

Output

You are in NameSpace1.MyClass

See Also

C# Keywords


Operator Keywords

This section introduces the following keywords:

  • as Convert an object to a compatible type.
  • is Check the run-time type of an object.
  • new
    • new operator Create objects.
    • new modifier Hide an inherited member.
  • sizeof Obtain the size of a type.
  • typeof Obtain the System.Type object for a type.
  • true Return the boolean value true.
  • false Return the boolean value false.
  • stackalloc Allocates a block of memory on the stack.

The following keywords, which can be used as operators and as statements, are covered in the Statements section:

  • checked Specify checked context.
  • unchecked Specify unchecked context.

See Also

C# Keywords


as

The as operator is used to perform conversions between compatible types. The as operator is used in an expression of the form:

expression as type

where:

expression
An expression of a reference type.
type
A reference type.

Remarks

The as operator is like a cast except that it yields null on conversion failure instead of raising an exception. More formally, an expression of the form:

expression as type

is equivalent to:

expression is type ? (type)expression : (type)null

except that expression is evaluated only once.

Example

// The as operator
using System;
class MyClass1
{
}

class MyClass2
{
}

public class IsTest
{
   public static void Main()
   {
      object [] myObjects = new object[6];
      myObjects[0] = new MyClass1();
      myObjects[1] = new MyClass2();
      myObjects[2] = "hello";
      myObjects[3] = 123;
      myObjects[4] = 123.4;
      myObjects[5] = null;

      for (int i=0; i<myObjects.Length; ++i)
      {
         string s = myObjects[i] as string;
         Console.Write ("{0}:", i);
         if (s != null)
            Console.WriteLine ( "'" + s + "'" );
         else
            Console.WriteLine ( "not a string" );
      }
   }
}

Output

0:not a string
1:not a string
2:'hello'
3:not a string
4:not a string
5:not a string

See Also

C# Keywords | is | ?: | Operator Keywords


is

The is operator is used to check whether the run-time type of an object is compatible with a given type. The is operator is used in an expression of the form:

expression is type
where:
expression
An expression of a reference type.
type
A type.

Remarks

An is expression evaluates to true if both of the following conditions are met:

  • expression is not null.
  • expression can be cast to type. That is, a cast expression of the form (type)(expression) will complete without throwing an exception. For more information, see §7.6.8 Cast expressions.

A compile-time warning will be issued if the expression expression is type is known to always be true or always be false.

The is operator cannot be overloaded.

Example

// The is operator
using System;
class Class1
{
}

class Class2
{
}

public class IsTest
{
   public static void Test (object o)
   {
      Class1   a;
      Class2   b;

      if (o is Class1)
      {
         Console.WriteLine ("o is Class1");
         a = (Class1)o;
         // do something with a
      }

      else if (o is Class2)
      {
         Console.WriteLine ("o is Class2");
         b = (Class2)o;
         // do something with b
      }

      else
      {
         Console.WriteLine ("o is neither Class1 nor Class2.");
      }
   }
   public static void Main()
   {
      Class1 c1 = new Class1();
      Class2 c2 = new Class2();
      Test (c1);
      Test (c2);
      Test ("a string");
   }
}

Output

o is Class1
o is Class2
o is neither Class1 nor Class2.

See Also

C# Keywords | typeof | as | Operator Keywords


new

In C#, the new keyword can be used as an operator or as a modifier.

  • new operator Used to create objects on the heap and invoke constructors.
  • new modifier Used to hide an inherited member from a base class member.

new Operator

The new operator is used to create objects and invoke constructors, for example:

Class1 MyClass  = new Class1();

It is also used to invoke the default constructor for value types, for example:

int myInt = new int();

Remarks

Remember that it is an error to declare a default constructor for a struct, because every value type implicitly has a public default constructor. It is possible though to declare parameterized constructors on a struct type.

Value-type objects such as structs are created on the stack, while reference-type objects such as classes are created on the heap.

The new operator cannot be overloaded.

Example

In the following example, a struct object and a class object are created and initialized by using the new operator and then assigned values. The default and the assigned values are displayed

// The new operator
using System;
class NewTest
{
   struct MyStruct
   {
      public int x;
      public int y;
      public MyStruct (int x, int y)
      {
         this.x = x;
         this.y = y;
      }
   }

   class MyClass
   {
      public string name;
      public int id;

      public MyClass ()
      {
      }

      public MyClass (int id, string name)
      {
         this.id = id;
         this.name = name;
      }
   }

   public static void Main()
   {
      // Create objects using default constructors:
      MyStruct Location1 = new MyStruct();
      MyClass Employee1 = new MyClass();

      // Display values:
      Console.WriteLine("Default values:");
      Console.WriteLine("\tStruct members: {0}, {1}",
         Location1.x, Location1.y);
      Console.WriteLine("\tClass members: {0}, {1}",
         Employee1.name, Employee1.id);

      // Create objects using parameterized constructors::
      MyStruct Location2 = new MyStruct(10, 20);
      MyClass Employee2 = new MyClass(1234, "John Martin Smith");

      // Display values:
      Console.WriteLine("Assigned values:");
      Console.WriteLine("\tStruct members: {0}, {1}",
         Location2.x, Location2.y);
      Console.WriteLine("\tClass members: {0}, {1}",
         Employee2.name, Employee2.id);
   }
}

Output

Default values:
        Struct members: 0, 0
        Class members: , 0
Assigned values:
        Struct members: 10, 20
        Class members: John Martin Smith, 1234

Notice in the example, that the default value of a string is null. Therefore, it was not displayed.

new Modifier

Use the new modifier to explicitly hide a member inherited from a base class. To hide an inherited member, declare it in the derived class using the same name, and modify it with the new modifier.

Consider the following class:

public class MyBaseC
{
   public int x;
   public void Invoke();
}

Declaring a member with the name M in a derived class, for example:

public class MyDerivedC : MyBaseC
{
   new public void Invoke();
}

will hide the method Invoke() in the base class. However, the field x will not be affected because it is not hidden by a similar name.

Name hiding through inheritance takes one of the following forms:

  • A constant, field, property, or type introduced in a class or struct hides all base class members with the same name.
  • A method introduced in a class or struct hides properties, fields, and types, with the same name, in the base class. It also hides all base class methods with the same signature. For more information see, §3.4 Signatures and overloading.
  • An indexer introduced in a class or struct hides all base class indexers with the same signature.
  • It is an error to use both new and override on the same member.
  • Using new and virtual together guarantees a new point of specialization.
  • Using the new modifier in a declaration that does not hide an inherited member generates a warning.

For more information on hiding names, see §3.5.1 Name hiding.

For more information on fully qualified names, see §3.6.1 Fully qualified names.

Example

In this example a base class, MyBaseC, and a derived class, MyDerivedC, use the same field name x, thus hiding the value of the inherited field. The example demonstrates the use of the new modifier. It also demonstrates how to access the hidden members of the base class by using the fully qualified names.

// The new modifier
using System;
public class MyBaseC
{
   public static int x = 55;
   public static int y = 22;
}

public class MyDerivedC : MyBaseC
{
   new public static int x = 100;   // Name hiding
   public static void Main()
   {
      // Display the overlapping value of x:
      Console.WriteLine(x);

      // Access the hidden value of x:
      Console.WriteLine(MyBaseC.x);

      // Display the unhidden member y:
      Console.WriteLine(y);
   }
}

Output

100
55
22

If you remove the new modifier, the program will still compile and run, but you will get the warning:

SC0108: The keyword new is required on 'MyDerivedC.x' because it hides inherited member 'MyBaseC.x'.

You can also use the new modifier to modify a nested type if the nested type is hiding another type, as demonstrated in the following example.

Example

In this example, a nested class, MyClass, hides a class with the same name in the base class. The example demonstrates using the new modifier to eliminate the warning message, as well as accessing the hidden class members by using the fully qualified names.

// Using the new modifier with nested types
using System;
public class MyBaseC
{
   public class MyClass
   {
      public int x = 200;
      public int y;
   }
}

public class MyDerivedC : MyBaseC
{
   new public class MyClass   // nested type hiding the base type members
   {
      public int x = 100;
      public int y;
      public int z;
   }

   public static void Main()
   {
      // Creating object from the overlapping class:
      MyClass S1  = new MyClass();

      // Creating object from the hidden class:
      MyBaseC.MyClass S2 = new MyBaseC.MyClass();

      Console.WriteLine(S1.x);
      Console.WriteLine(S2.x);
   }
}

Output

100
200

See Also

C# Keywords | Operator Keywords | Modifiers


sizeof

The sizeof operator is used to obtain the size in bytes for a value type. A sizeof expression takes the form:

sizeof(type)

where:

type
The value type for which the size is obtained.

Remarks

The sizeof operator can be applied only to value types, not reference types.

The sizeof operator can only be used in the unsafe mode. That is, in an unsafe member or inside an unsafe block.

The sizeof operator cannot be overloaded.

Example

// Using the sizeof operator
using System;
class SizeClass
{
   // Notice the unsafe declaration of the method:
   unsafe public static void SizesOf()
   {
      Console.WriteLine("The size of short is {0}.", sizeof(short));
      Console.WriteLine("The size of int is {0}.", sizeof(int));
      Console.WriteLine("The size of long is {0}.", sizeof(long));
   }
}

class MainClass
{
   public static void Main()
   {
      SizeClass.SizesOf();
   }
}

Output

The size of short is 2.
The size of int is 4.
The size of long is 8.

See Also

C# Keywords | Operator Keywords


typeof

The typeof operator is used to obtain the System.Type object for a type. A typeof expression takes the form:

typeof(type)

where:

type
The type for which the System.Type object is obtained.

Remarks

The typeof operator cannot be overloaded.

To obtain the run-time type of an expression, you can use the .NET Framework method GetType.

Example

using System;
using System.Reflection;

public class MyClass
{
   public int intI;
   public void MyMeth()
   {
   }

   public static void Main()
   {
      Type t = typeof(MyClass);

      // alternatively, you could use
      // MyClass t1 = new MyClass();
      // Type t = t1.GetType();

      MethodInfo[] x = t.GetMethods();
      foreach (MethodInfo xtemp in x)
      {
         Console.WriteLine(xtemp.ToString());
      }

      Console.WriteLine();

      MemberInfo[] x2 = t.GetMembers();
      foreach (MemberInfo xtemp2 in x2)
      {
         Console.WriteLine(xtemp2.ToString());
      }
   }
}

Output

Int32 GetHashCode ()
Boolean Equals (System.Object)
System.String ToString ()
Void MyMeth ()
Void Main ()
System.Type GetType ()

Int32 intI
Int32 GetHashCode ()
Boolean Equals (System.Object)
System.String ToString ()
Void MyMeth ()
Void Main ()
System.Type GetType ()
Void .ctor ()

Example

// Using GetType method
using System;
class GetTypeTest
{
   public static void Main()
   {
      int radius = 3;
      Console.WriteLine("Area = {0} ", radius*radius*Math.PI);
      Console.WriteLine("The type is {0}",
               (radius*radius*Math.PI).GetType());
   }
}

Output

Area = 28.274333882308138
The type is Double

See Also

C# Keywords | is | Operator Keywords


true

In C#, the true keyword can be used as an overloaded operator or as a literal:

  • true Operator
  • true Literal

true Operator

User-defined types can define a true operator that returns the bool value true to indicate true and returns false otherwise. This is useful for types that represent true, false, and null (neither true nor false), as used in databases.

Such types can be used for the controlling expression in if, do, while, and for statements and in conditional expressions.

If a type defines operator true, it must also define operator false.

A type cannot directly overload the conditional logical operators (&& and ||), but an equivalent effect can be achieved by overloading the regular logical operators and operators true and false (see §7.11.2 User-defined conditional logical operators).

Example

See the example in §11.3.2 Database boolean type.

true Literal

The true keyword is a literal of type bool representing the boolean value true.

Example

using System;
class test
{
   public static void Main()
   {
      bool a = true;
      Console.WriteLine( a ? "yes" : "no" );
   }
}

Output

yes

See Also

C# Keywords | C# Operators | false | Operator Overloading Tutorial


false

In C#, the false keyword can be used as an overloaded operator or as a literal:

  • false Operator
  • false Literal

false Operator

User-defined types can define a false operator that returns the bool value true to indicate false and returns false otherwise. This is useful for types that represent true, false, and null (neither true nor false), as used in databases.

Such types can be used for the controlling expression in if, do, while, and for statements and in conditional expressions.

If a type defines operator false, it must also define operator true.

A type cannot directly overload the conditional logical operators (&& and ||), but an equivalent effect can be achieved by overloading the regular logical operators and operators true and false (see §7.11.2 User-defined conditional logical operators).

Example

See the example in §11.3.2 Database boolean type.

false Literal

The false keyword is a literal of type bool representing the boolean value false.

Example

using System;
class test
{
   public static void Main()
   {
      bool a = false;
      Console.WriteLine( a ? "yes" : "no" );
   }
}

Output

no

See Also

C# Keywords | C# Operators | true | Operator Overloading Tutorial


stackalloc

Allocates a block of memory on the stack.

type * ptr = stackalloc type [ expr ];

where:

type
An unmanaged type.
ptr
A pointer name.
expr
An integral expression.

Remarks

A block of memory of sufficient size to contain expr elements of type type is allocated on the stack, not the heap; the address of the block is stored in pointer ptr. This memory is not subject to garbage collection and therefore does not have to be pinned (via fixed). The lifetime of the memory block is limited to the lifetime of the method in which it is defined.

stackalloc is only valid in local variable initializers.

Because pointers are involved, stackalloc requires unsafe context.

stackalloc is similar to _alloca in the C run-time library.

Example

using System;   // compile with /unsafe option
class Test
{
   public static unsafe void Main()
   {
      int* fib = stackalloc int[100];
      int* p = fib;
      *p++ = *p++ = 1;
      for (int i=2; i<100; ++i, ++p)
*p = p[-1] + p[-2];
      for (int i=0; i<10; ++i)
Console.WriteLine (fib[i]);
   }
}

Output

1
1
2
3
5
8
13
21
34
55

See Also

C# Keywords | Operator Keywords


Conversion Keywords

This section describes keywords used in type conversions:

  • explicit
  • implicit
  • operator

See Also

C# Keywords


explicit

The explicit keyword is used to declare an explicit user-defined type conversion operator (§6.4.4 User-defined explicit conversions).

class MyType
{
   public static explicit operator MyType(int i)
   {
      // code to convert from int to MyType
   }
}

Unlike implicit conversion, explicit conversion operators must be invoked via a cast.

int i;
MyType x = (MyType)i;   // int-to-MyType requires cast

Omitting the cast results in a compile-time error.

If a conversion operation can cause exceptions or lose information, you should mark it explicit. This prevents the compiler from silently invoking the conversion operation with possibly unforeseen consequences.

Example

The following example defines a struct, Digit, that represents a single decimal digit. An operator is defined for conversions from byte to Digit, but because not all bytes can be converted to a Digit, the conversion is explicit.

using System;
struct Digit
{
   byte value;
   public Digit(byte value)
   {
      if (value<0 || value>9) throw new ArgumentException();
      this.value = value;
   }

   // define explicit byte-to-Digit conversion operator:
   public static explicit operator Digit(byte b)
   {
      Console.WriteLine("conversion occurred");
      return new Digit(b);
   }
}

class Test
{
   public static void Main()
   {
      byte b = 3;
      Digit d = (Digit)b; // explicit conversion
   }
}

Output

conversion occurred

See Also

C# Keywords | implicit | operator | User-Defined Conversions Tutorial


implicit

The implicit keyword is used to declare an implicit user-defined type conversion operator (§6.4.3 User-defined implicit conversions). For example:

class MyType
{
   public static implicit operator int(MyType m)
   {
      // code to convert from MyType to int
   }
}

Implicit conversion operators can be called implicitly, without being specified by explicit casts in the source code.

MyType x;
int i = x; // implicitly call MyType's MyType-to-int conversion operator

By eliminating unnecessary casts, implicit conversions can improve source code readability. However, because implicit conversions can occur without the programmer's specifying them, care must be taken to prevent unpleasant surprises. In general, implicit conversion operators should never throw exceptions and never lose information so that they can be used safely without the programmer's awareness. If a conversion operator cannot meet those criteria, it should be marked explicit.

Example

The following example defines a struct, Digit, that represents a single decimal digit. An operator is defined for conversions from Digit to byte, and because any Digit can be converted to a byte, the conversion is implicit.

using System;
struct Digit
{
   byte value;

   public Digit(byte value)
   {
      if (value < 0 || value > 9) throw new ArgumentException();
      this.value = value;
   }

   // define implicit Digit-to-byte conversion operator:
   public static implicit operator byte(Digit d)
   {
      Console.WriteLine( "conversion occurred" );
      return d.value;
   }
}

class Test
{
   public static void Main()
   {
      Digit d = new Digit(3);

      // implicit (no cast) conversion from Digit to byte
      byte b = d;
   }
}

Output

conversion occurred

See Also

C# Keywords | explicit | operator | User-Defined Conversions Tutorial


operator

The operator keyword is used to declare an operator in a class or struct declaration. An operator declaration can take one of the following four forms:

public static result-type operator unary-operator ( op-type operand )
public static result-type operator binary-operator (
 op-type operand,
 op-type2 operand2
)
public static implicit operator conv-type-out ( conv-type-in operand )
public static explicit operator conv-type-out ( conv-type-in operand )

where:

result-type
The type of the operator's result.
unary-operator
One of: + - ! ~ ++ — true false
op-type
The type of the first (or only) parameter.
operand
The name of the first (or only) parameter.
binary-operator
One of: + - * / % & | ^ << >> == != > < >= <=
op-type2
The type of the second parameter.
operand2
The name of the second parameter.
conv-type-out
The target type of a type conversion operator.
conv-type-in
The type of the input to a type conversion operator.

Remarks

The first two forms declare user-defined operators that overload built-in operators. Note that not all the built-in operators can be overloaded (see Overloadable Operators). At least one of op-type and op-type2 must be the enclosing type (that is, the type of which the operator is a member). This prevents redefining the integer addition operator, for instance.

The last two forms declare conversion operators. Exactly one of conv-type-in and conv-type-out must be the enclosing type (that is, a conversion operator can only convert from its enclosing type to some other type, or from some other type to its enclosing type).

Operators can only take value parameters, not ref or out parameters.

Any operator declaration can be preceded by an optional attribute list.

For more information, refer to §10.9 Operators.

Example

The following is an extremely simplified class for rational numbers. It overloads the + and * operators to perform fractional addition and multiplication, and also provides an operator that converts fractions to doubles.

using System;
class Fraction
{
   int num, den;
   public Fraction(int num, int den)
   {
      this.num = num;
      this.den = den;
   }

   // overload operator +
   public static Fraction operator +(Fraction a, Fraction b)
   {
      return new Fraction(a.num * b.den + b.num * a.den,
         a.den * b.den);
   }

   // overload operator *
   public static Fraction operator *(Fraction a, Fraction b)
   {
      return new Fraction(a.num * b.num, a.den * b.den);
   }

   // define operator double
   public static implicit operator double(Fraction f)
   {
      return (double)f.num / f.den;
   }
}

class Test
{
   public static void Main()
   {
      Fraction a = new Fraction(1, 2);
      Fraction b = new Fraction(3, 7);
      Fraction c = new Fraction(2, 3);
      Console.WriteLine((double)(a * b + c));
   }
}

Output

0.88095238095238093

See Also

C# Keywords | implicit | explicit | User-Defined Conversions Tutorial


Access Keywords

This section introduces the following access keywords:

  • base Access the members of the base class.
  • this Refer to the current object for which a method is called.

See Also

C# Keywords


base

The base keyword is used to access members of the base class from within a derived class:

  • Call a method on the base class that has been overridden by another method.
  • Specify which base-class constructor should be called when creating instances of the derived class.

A base class access is permitted only in a constructor, an instance method, or an instance property accessor.

It is an error to use the base keyword from within a static method.

Example

In this example, both the base class, Person, and the derived class, Employee, have a method named Getinfo. By using the base keyword, it is possible to call the Getinfo method on the base class, from within the derived class.

// Accessing base class members
using System;
   public class Person
   {
      protected string ssn = "444-55-6666";
      protected string name = "John L. Malgraine";

      public virtual void GetInfo()
      {
         Console.WriteLine("Name: {0}", name);
         Console.WriteLine("SSN: {0}", ssn);
      }
   }
   class Employee: Person
   {
      public string id = "ABC567EFG";

      public override void GetInfo()
      {
         // Calling the base class GetInfo method:
         base.GetInfo();
         Console.WriteLine("Employee ID: {0}", id);
      }
   }

class TestClass {
   public static void Main()
   {
      Employee E = new Employee();
      E.GetInfo();
   }
}

Output

Name: John L. Malgraine
SSN: 444-55-6666
Employee ID: ABC567EFG

For additional examples, see new, virtual, and override.

Example

This sample shows how you can specify the base-class constructor called when creating instances of a derived class.

using System;
public class MyBase
{
   int num;
   public MyBase() {
      Console.WriteLine("in MyBase()");
   }
   public MyBase(int i )
   {
      num = i;
      Console.WriteLine("in MyBase(int i)");
   }

   public int GetNum()
   {
      return num;
   }
}

public class MyDerived : MyBase
{
   static int i = 32;

   // this constructor will call Base::Base()
   public MyDerived(int ii)  : base()
   {
   }

   // this constructor will call Base::Base(int i)
   public MyDerived()  : base(i)
   {
   }

   public static void Main()
{
      MyDerived md = new MyDerived();
      MyDerived md1 = new MyDerived(1);
   }
}

Output

in MyBase(int i)
in MyBase()

See Also

C# Keywords | this


this

The this keyword refers to the current instance for which a method is called. Static member functions do not have a this pointer. The this keyword can be used to access members from within constructors, instance methods, and instance accessors.

The following are common uses of this:

  • To qualify members hidden by similar names, for example:
    public Employee(string name, string alias)
{
   this.name = name;
   this.alias = alias;
}
  • To pass an object as a parameter to other methods, for example:
    CalcTax(this);
  • To declare indexers, for example:
    public int this [int param]
{
      get
      {
         return array[param];
      }
      set
      {
         array[param] = value;
      }
   }

It is an error to refer to this in a static method, static property accessor, or variable initializer of a field declaration.

Example

In this example, this is used to qualify the Employee class members, name and alias, which are hidden by similar names. It is also used to pass an object to the method CalcTax, which belongs to another class.

// this example
using System;
public class Employee
{
   public string name;
   public string alias;
   public decimal salary = 3000.00m;

   // Constructor:
   public Employee(string name, string alias)
   {
      // Use this to qualify the fields, name and alias:
      this.name = name;
      this.alias = alias;
   }

   // Printing method:
   public void printEmployee()
   {
      Console.WriteLine("Name: {0}\nAlias: {1}", name, alias);
      // Passing the object to the CalcTax method by using this:
      Console.WriteLine("Taxes: {0}", decimal.Format(Tax.CalcTax(this), "C"));
   }
}
public class Tax
{
   public static decimal CalcTax(Employee E)
   {
      return (0.08m*(E.salary));
   }
}

public class MainClass
{
   public static void Main()
   {
      // Create objects:
      Employee E1 = new Employee ("John M. Trainer", "jtrainer");

      // Display results:
      E1.printEmployee();
   }
}

Output

Name: John M. Trainer
Alias: jtrainer
Taxes: $240.00

For additional examples, see class and struct.

See Also

C# Keywords | base


Literal Keywords

C# has the following literal keywords:

  • null
  • true
  • false

See Also

C# Keywords


null

The null keyword is a literal that represents a null reference, one that does not refer to any object. null is the default value of reference-type variables.

See Also

C# Keywords | Literal keywords | Default Values Table


C# Operators

C# provides a large set of operators, which are symbols that specify which operations to perform in an expression. C# predefines the usual arithmetic and logical operators, as well as a variety of others as shown in the following table. In addition, many operators can be overloaded by the user, thus changing their meaning when applied to a user-defined type.

Operator category | Operators
Arithmetic | + - * / %
Logical (boolean and bitwise) | & | ^ ! ~ && || true false
String concatenation | +
Increment, decrement | ++ —
Shift | << >>
Relational | == != < > <= >=
Assignment | = += -= *= /= %= &= |= ^= <<= >>=
Member access | .
Indexing | []
Cast | ()
Conditional | ?:
Delegate concatenation and removal | + -
Object creation | new
Type information | is sizeof typeof
Overflow exception control | checked unchecked
Indirection and Address | * -> [] &

Arithmetic Overflow

The arithmetic operators (+, -, *, /) can produce results that are outside the range of possible values for the numeric type involved. You should refer to the C# Language Reference section on a particular operator for details, but in general:

  • Integer arithmetic overflow either throws an OverflowException or discards the most significant bits of the result (see below). Integer division by zero always throws a DivideByZeroException.
  • Floating-point arithmetic overflow or division by zero never throws an exception, because floating-point types are based on IEEE 754 and so have provisions for representing infinity and NaN (Not a Number).
  • Decimal arithmetic overflow always throws an OverflowException. Decimal division by zero always throws a DivideByZeroException.

When integer overflow occurs, what happens depends on the execution context, which can be checked or unchecked. In a checked context, an OverflowException is thrown. In an unchecked context, the most significant bits of the result are discarded and execution continues. Thus, C# gives you the choice of handling or ignoring overflow.

In addition to the arithmetic operators, integral-type to integral-type casts can cause overflow (for example, casting a long to an int) and are subject to checked or unchecked execution. Also note that bitwise operators and shift operators never cause overflow.

See Also

§7.2.1 Operator precedence and associativity | Overloadable Operators | C# Keywords | C# Programmer's Reference


Overloadable Operators

C# allows user-defined types to overload operators by defining static member functions using the operator keyword. Not all operators can be overloaded, however, and others have restrictions, as listed in this table:

Operators | Overloadability
+, -, !, ~, ++, —, true, false | These unary operators can be overloaded.
+, -, *, /, %, &, |, ^, <<, >> | These binary operators can be overloaded.
==, !=, <, >, <=, >= | The comparison operators can be overloaded (but see note below).
&&, || | The conditional logical operators cannot be overloaded, but they are evaluated using & and |, which can be overloaded; see §7.11.2 User-defined conditional logical operators.
[] | The array indexing operator cannot be overloaded, but you can define indexers.
() | The cast operator cannot be overloaded, but you can define new conversion operators (see explicit and implicit).
+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>= | Assignment operators cannot be overloaded, but +=, for example, is evaluated using +, which can be overloaded.
=, ., ?:, ->, new, is, sizeof, typeof | These operators cannot be overloaded.

Note The comparison operators, if overloaded, must be overloaded in pairs; that is, if == is overloaded, != must also be overloaded. The reverse is also true, and similarly for < and >, and for <= and >=.

See Also

C# Operators | Operator Overloading Tutorial


[] Operator

Square brackets ([]) are used for arrays, indexers, and attributes. They can also be used with pointers.

An array type is a type followed by []:

int[] fib; // fib is of type int[], "array of int"
fib = new int[100]; // create a 100-element int array

To access an element of an array, the index of the desired element is enclosed in brackets:

fib[0] = fib[1] = 1;
for( int i=2; i<100; ++i ) fib[i] = fib[i-1] + fib[i-2];

An exception is thrown if an array index is out of range.

The array indexing operator cannot be overloaded; however, types can define indexers, properties that take one or more parameters. Indexer parameters are enclosed in square brackets, just like array indices, but indexer parameters can be declared to be of any type (unlike array indices, which must be integral).

For example, the .NET Framework defines a Hashtable type which associates keys and values of arbitrary type:

Collections.Hashtable h =  new Collections.Hashtable();
h["a"] = 123; // note: using a string as the index

Square brackets are also used to specify attributes:

[attribute(AllowMultiple=true)]
public class Attr {
}

You can use square brackets to index off a pointer.

unsafe fixed ( int* p = fib )   // p points to fib from earlier example
{
   p[0] = p[1] = 1;
   for( int i=2; i<100; ++i ) p[i] = p[i-1] + p[i-2];
}

No bounds checking is performed.

See Also

C# Operators | Arrays | Indexers Unsafe and Fixed


() Operator

In addition to being used to specify the order of operations in an expression, parentheses are used to specify casts (type conversions):

( type ) expr
where:
type
The name of the type to which expr is to be converted.
expr
An expression.

Remarks

A cast explicitly invokes the conversion operator from expr's type to type; the cast will fail if no such conversion operator is defined. To define a conversion operator, see explicit and implicit.

Example

The following program casts a double to an int. The program won't compile without the cast.

using System;
class Test
{
   public static void Main()
   {
      double x = 1234.7;
      int a;
      a = (int)x; // cast double to int
      Console.WriteLine(a);
   }
}

Output

1234

See Also

C# Operators | §7.6.8 Cast expressions | explicit


. Operator

The dot operator is used for member access. For example, consider the following class:

class Simple
{
   public int a;
   public void b()
   {
   }
}
Simple s = new Simple();

The variable s has two members, a and b; to access them, use the dot operator:

s.a = 6;   // assign to field a;
s.b();   // invoke member function b;

The dot is also used to form qualified names, names that specify the namespace or interface (for example) to which they belong.

System.Console.WriteLine("hello"); // class Console in namespace System

The using directive makes some name qualification optional:

using System;
...
System.Console.WriteLine("hello");
Console.WriteLine("hello");   // same thing

But when an identifier is ambiguous, it must be qualified:

using System;
using OtherSystem; // a namespace containing another Console class
...
System.Console.WriteLine( "hello" ); // must qualify Console

See Also

C# Operators | §7.5.4 Member access


+ Operator

The + operator can function as either a unary or a binary operator.

Unary + operators are predefined for all numeric types. The result of a unary + operation on a numeric type is simply the value of the operand.

Binary + operators are predefined for numeric and string types. For numeric types, + computes the sum of its two operands. When one or both operands are of type string, + concatenates the string representations of the operands.

Delegate types also provide a binary + operator, which performs delegate concatenation.

User-defined types can overload the unary + and binary + operators (see operator).

Example

using System;
class Test
{
   public static void Main()
   {
      Console.WriteLine(+5);        // unary plus
      Console.WriteLine(5 + 5);     // addition
      Console.WriteLine(5 + .5);    // addition
      Console.WriteLine("5" + "5"); // string concatenation
      Console.WriteLine(5.0 + "5"); // string concatenation
      // note automatic conversion from double to string
   }
}

Output

5
10
5.5
55
55

See Also

C# Operators | §7.6.1 Unary plus operator | §7.7.4 Addition operator


- Operator

The - operator can function as either a unary or a binary operator.

Unary - operators are predefined for all numeric types. The result of a unary - operation on a numeric type is the negation of the operand.

Binary - operators are predefined for all numeric and enumeration types to subtract the second operand from the first.

Delegate types also provide a binary - operator, which performs delegate removal.

User-defined types can overload the unary – and binary - operators (see operator).

Example

using System;
class Test
{
   public static void Main()
   {
      int a = 5;
      Console.WriteLine(-a);
      Console.WriteLine(a - 1);
      Console.WriteLine(a - .5);
   }
}

Output

-5
4
4.5

See Also

C# Operators | §7.6.2 Unary minus operator | §7.7.5 Subtraction operator


* Operator

The multiplication operator (*) computes the product of its operands. All numeric types have predefined multiplication operators.

The * operator is also used to declare pointer types and to dereference pointers.

User-defined types can overload the * operator (see operator).

Example

using System;
class Test
{
   public static void Main()
   {
      Console.WriteLine(5 * 2);
      Console.WriteLine(-.5 * .2);
      Console.WriteLine(-.5m * .2m); // decimal type
   }
}

Output

10
-0.10000000000000001
-0.1

Note that the double type cannot represent the value -0.1 exactly, while the decimal type can.

See Also

C# Operators | §7.7.1 Multiplication operator


/ Operator

The division operator (/) divides its first operand by its second. All numeric types have predefined division operators.

User-defined types can overload the / operator (see operator).

Example

using System;
class Test
{
   public static void Main()
   {
      Console.WriteLine(-5/2);
      Console.WriteLine(-5.0/2);
   }
}

Output

-2
-2.5

See Also

C# Operators | §7.7.2 Division operator


% Operator

The modulus operator (%) computes the remainder after dividing its first operand by its second. All numeric types have predefined modulus operators.

User-defined types can overload the % operator (see operator).

Example

using System;
class Test
{
   public static void Main()
   {
      Console.WriteLine(5 % 2);   // int
      Console.WriteLine(-5 % 2);   // int
      Console.WriteLine(5.0 % 2.2);   // double
      Console.WriteLine(5.0m % 2.2m);   // decimal
      Console.WriteLine(-5.2 % 2.0);   // double
   }
}

Output

1
-1
0.59999999999999964
0.6
-1.2000000000000002

Note the round-off errors associated with the double type.

See Also

C# Operators | §7.7.3 Remainder operator


& Operator

The & operator can function as either a unary or a binary operator.

The unary & operator returns the address of its operand (requires unsafe context).

Binary & operators are predefined for the integral types and bool. For integral types, & computes the bitwise AND of its operands. For bool operands, & computes the logical AND of its operands; that is, the result is true if and only if both its operands are true.

User-defined types can overload the binary & operator (see operator).

Example

using System;
class Test
{
   public static void Main() {
      Console.WriteLine(true & false); // logical and
      Console.WriteLine(true & true);  // logical and
      Console.WriteLine("0x{0:x}", 0xf8 & 0x3f);  // bitwise and
   }
}

Output

False
True
0x38

See Also

C# Operators | §7.6.6 Address operator | §7.10 Logical operators


| Operator

Binary | operators are predefined for the integral types and bool. For integral types, & computes the bitwise OR of its operands. For bool operands, | computes the logical OR of its operands; that is, the result is false if and only if both its operands are false.

User-defined types can overload the | operator (see operator).

Example

using System;
class Test
{
   public static void Main()
   {
      Console.WriteLine(true | false);  // logical or
      Console.WriteLine(false | false); // logical or
      Console.WriteLine("0x{0:x}", 0xf8 | 0x3f);   // bitwise or
   }
}

Output

True
False
0xff

See Also

C# Operators | §7.10 Logical operators


^ Operator

Binary ^ operators are predefined for the integral types and bool. For integral types, ^ computes the bitwise exclusive-OR of its operands. For bool operands, ^ computes the logical exclusive-or of its operands; that is, the result is true if and only if exactly one of its operands is true.

User-defined types can overload the ^ operator (see operator).

Example

using System;
class Test
{
   public static void Main()
   {
      Console.WriteLine(true ^ false);  // logical exclusive-or
      Console.WriteLine(false ^ false); // logical exclusive-or
      Console.WriteLine("0x{0:x}", 0xf8 ^ 0x3f);   // bitwise exclusive-or
   }
}

Output

True
False
0xc7

See Also

C# Operators | §7.10.1 Integer logical operators


! Operator

The logical negation operator (!) is a unary operator that negates its operand. It is defined for bool and returns true if and only if its operand is false.

User-defined types can overload the ! operator (see operator).

Example

using System;
class Test
{
   public static void Main()
{
      Console.WriteLine(!true);
      Console.WriteLine(!false);
   }
}

Output

False
True

See Also

C# Operators | §7.6.3 Logical negation operator


~ Operator

The ~ operator performs a bitwise complement operation on its operand. Bitwise complement operators are predefined for int, uint, long, and ulong.

User-defined types can overload the ~ operator (see operator).

Example

using System;
class Test
{
   public static void Main()
   {
      Console.WriteLine("!0x{0:x8} = 0x{1:x8}", 8, ~8);
      Console.WriteLine("!0x{0:x8} = 0x{1:x8}", -8, ~-8);
   }
}

Output

!0x00000008 = 0xfffffff7
!0xfffffff8 = 0x00000007

See Also

C# Operators | §7.6.4 Bitwise complement operator


= Operator

The assignment operator (=) stores the value of its right-hand operand in the storage location, property, or indexer denoted by its left-hand operand and returns the value as its result. The operands must be of the same type (or the right-hand operand must be implicitly convertible to the type of the left-hand operand).

The assignment operator cannot be overloaded.

Example

using System;
class Test
{
   public static void Main()
   {
      double x;
      int i;
      i = 5; // int to int assignment
      x = i; // implicit conversion from int to double
      i = (int)x; // needs cast
      Console.WriteLine("i is {0}, x is {1}", i, x);
      object obj = i;
      Console.WriteLine("boxed value = {0}, type is {1}",
         obj, obj.GetType());
      i = (int)obj;
      Console.WriteLine("unboxed: {0}", i);
   }
}

Output

i is 5, x is 5
boxed value = 5, type is Int32
unboxed: 5

See Also

C# Operators | §7.13 Assignment operators


< Operator

All numeric and enumeration types define a "less than" relational operator (<) that returns true if the first operand is less than the second, false otherwise.

User-defined types can overload the < operator (see operator). If < is overloaded, > must also be overloaded.

Example

using System;
class Test
{
   public static void Main()
   {
      Console.WriteLine(1 < 1.1);
      Console.WriteLine(1.1 < 1.1);
   }
}

Output

True
False

See Also

C# Operators | §7.9 Relational operators


> Operator

All numeric and enumeration types define a "greater than" relational operator (>) that returns true if the first operand is greater than the second, false otherwise.

User-defined types can overload the > operator (see operator). If > is overloaded, < must also be overloaded.

Example

using System;
class Test
{
   public static void Main()
   {
      Console.WriteLine(1.1 > 1);
      Console.WriteLine(1.1 > 1.1);
   }
}

Output

True
False

See Also

C# Operators | §7.9 Relational operators


?: Operator

The conditional operator (?:) returns one of two values depending on a third value. The conditional operator is used in an expression of the following form:

cond-expr ? expr1 : expr2
where:
cond-expr
An expression of type bool.
expr1
An expression.
expr2
An expression.

Remarks

If cond-expr is true, expr1 is evaluated and becomes the result; if cond-expr is false, expr2 is evaluated and becomes the result. Only one of expr1 and expr2 is ever evaluated.

Calculations that might otherwise require an if-else construction can be expressed more concisely and elegantly with the conditional operator. For example, to avoid a division by zero in the calculation of the sinc function you could write either

if(x != 0.0) s = Math.Sin(x)/x; else s = 1.0;

or, using the conditional operator,

s = x != 0.0 ? Math.Sin(x)/x : 1.0;

The conditional operator is right-associative, so an expression of the form

a ? b : c ? d : e

is evaluated as

a ? b : (c ? d : e)

not

(a ? b : c) ? d : e

The conditional operator cannot be overloaded.

Example

using System;
class Test
{
   public static double sinc(double x)
   {
      return x != 0.0 ? Math.Sin(x)/x : 1.0;
   }

   public static void Main()
   {
      Console.WriteLine(sinc(0.2));
      Console.WriteLine(sinc(0.1));
      Console.WriteLine(sinc(0.0));
   }
}

Output

0.99334665397530608
0.99833416646828155
1

See Also

C# Operators | §7.12 Conditional operator


++ Operator

The increment operator (++) increments its operand by 1. The increment operator can appear before or after its operand:

++ var
var ++
where:
var
An expression that denotes a storage location or a property or an indexer.

Remarks

The first form is a prefix increment operation. The result of the operation is the value of the operand after it has been incremented.

The second form is a postfix increment operation. The result of the operation is the value of the operand before it has been incremented.

Numeric and enumeration types have predefined increment operators. User-defined types can overload the ++ operator (see operator).

Example

using System;
class Test
{
   public static void Main()
   {
      double x;
      x = 1.5;
      Console.WriteLine(++x);
      x = 1.5;
      Console.WriteLine(x++);
      Console.WriteLine(x);
   }
}

Output

2.5
1.5
2.5

See Also

C# Operators | §7.5.9 Postfix increment and decrement operators | §7.6.7 Prefix increment and decrement operators


-- Operator

C# Programmer's Reference

— Operator

[This topic is part of a preview and is subject to change in future releases. Blank topics are included as placeholders.]

The decrement operator ( — ) decrements its operand by 1. The decrement operator can appear before or after its operand:

-- var
var --
where:
var
An expression that denotes a storage location or a property or an indexer.

Remarks

The first form is a prefix decrement operation. The result of the operation is the value of the operand after it has been decremented.

The second form is a postfix decrement operation. The result of the operation is the value of the operand before it has been decremented.

Numeric and enumeration types have predefined decrement operators. User-defined types can overload the — operator (see operator).

Example

using System;
class Test
{
   public static void Main()
   {
      double x;
      x = 1.5;
      Console.WriteLine(--x);
      x = 1.5;
      Console.WriteLine(x--);
      Console.WriteLine(x);
   }
}

Output

0.5
1.5
0.5

See Also

C# Operators | §7.5.9 Postfix increment and decrement operators | §7.6.7 Prefix increment and decrement operators


&& Operator

The conditional-AND operator (&&) performs a logical-AND of its bool operands, but only evaluates its second operand if necessary. That is, the operation

x && y

corresponds to the operation

x & y

except that if x is false, y is not evaluated (because the result of the AND operation is false no matter what the value of y may be). This is known as "short-circuit" evaluation.

The conditional-AND operator cannot be overloaded, but overloads of the regular logical operators and operators true and false are, with certain restrictions, also considered overloads of the conditional logical operators (see §7.11.2 User-defined conditional logical operators).

Example

In the following example, observe that the expression using && evaluates only the first operand.

using System;
class Test
{
   static bool fn1()
   {
      Console.WriteLine("fn1 called");
      return false;
   }

   static bool fn2()
   {
      Console.WriteLine("fn2 called");
      return true;
   }

   public static void Main()
   {
      Console.WriteLine("regular AND:");
      Console.WriteLine("result is {0}", fn1() & fn2());
      Console.WriteLine("short-circuit AND:");
      Console.WriteLine("result is {0}", fn1() && fn2());
   }
}

Output

regular AND:
fn1 called
fn2 called
result is False
short-circuit AND:
fn1 called
result is False

See Also

C# Operators | §7.11 Conditional logical operators


|| Operator

The conditional-OR operator (||) performs a logical-OR of its bool operands, but only evaluates its second operand if necessary. That is, the operation

x || y

corresponds to the operation

x | y

except that if x is true, y is not evaluated (because the result of the OR operation is true no matter what the value of y might be). This is known as "short-circuit" evaluation.

The conditional-OR operator cannot be overloaded, but overloads of the regular logical operators and operators true and false are, with certain restrictions, also considered overloads of the conditional logical operators (see §7.11.2 User-defined conditional logical operators).

Example

In the following example, observe that the expression using || evaluates only the first operand.

using System;
class Test
{
   static bool fn1()
   {
      Console.WriteLine("fn1 called");
      return true;
   }

   static bool fn2()
   {
      Console.WriteLine("fn2 called");
      return false;
   }

   public static void Main()
   {
      Console.WriteLine("regular OR:");
      Console.WriteLine("result is {0}", fn1() | fn2());
      Console.WriteLine("short-circuit OR:");
      Console.WriteLine("result is {0}", fn1() || fn2());
   }
}

Output

regular OR:
fn1 called
fn2 called
result is True
short-circuit OR:
fn1 called
result is True

See Also

C# Operators | §7.11 Conditional logical operators


<< Operator

The left-shift operator (<<) shifts its first operand left by the number of bits specified by its second operand.

expr << count
where:
expr
An expression of type int, uint, long, or ulong; the value to be shifted.
count
An expression of type int; the shift count.

Remarks

If expr is an int or uint (32-bit quantity), the shift count is given by the low-order five bits of count (count & 0x1f).

If expr is a long or ulong (64-bit quantity), the shift count is given by the low-order six bits of count (count & 0x3f).

The high-order bits of expr are discarded and the low-order empty bits are zero-filled. Shift operations never cause overflows.

User-defined types can overload the << operator (see operator); the type of the first operand must be the user-defined type, and the type of the second operand must be int.

Example

using System;
class Test
{
   public static void Main()
   {
      int i = 1;
      long lg = 1;
      Console.WriteLine("0x{0:x}", i << 1);
      Console.WriteLine("0x{0:x}", i << 33);
      Console.WriteLine("0x{0:x}", lg << 33);
   }
}

Output

0x2
0x2
0x200000000

Note that i<<1 and i<<33 give the same result, because 1 and 33 have the same low-order five bits.

See Also

C# Operators | §7.8 Shift operators | >> Operator


>> Operator

The right-shift operator (>>) shifts its first operand right by the number of bits specified by its second operand.

expr >> count
where:
expr
An expression of type int, uint, long, or ulong; the value to be shifted.
count
An expression of type int; the shift count.

Remarks

If expr is an int or uint (32-bit quantity), the shift count is given by the low-order five bits of count (count & 0x1f).

If expr is a long or ulong (64-bit quantity), the shift count is given by the low-order six bits of count (count & 0x3f).

If expr is an int or long, the right-shift is an arithmetic shift (high-order empty bits are set to the sign bit). If expr is of type uint or ulong, the right-shift is a logical shift (high-order bits are zero-filled).

User-defined types can overload the >> operator (see operator); the type of the first operand must be the user-defined type, and the type of the second operand must be int.

Example

using System;
class Test
{
   public static void Main()
   {
      int i = -1000;
      Console.WriteLine(i >> 3);
   }
}

Output

-125

See Also

C# Operators | §7.8 Shift operators | << Operator


== Operator

For predefined value types, the equality operator (==) returns true if the values of its operands are equal, false otherwise. For reference types other than string, == returns true if its two operands refer to the same object. For the string type, == compares the values of the strings.

User-defined value types can overload the == operator (see operator). So can user-defined reference types, although by default == behaves as described above for both predefined and user-defined reference types. If == is overloaded, != must also be overloaded.

Example

using System;
class Test
{
   public static void Main()
   {
      // Numeric equality: True
      Console.WriteLine((2 + 2) == 4);

      // Reference equality: different objects, same boxed value: False
      object s = 1;
      object t = 1;
      Console.WriteLine(s == t);

      // Define some string
      string a = "hello";
      string b = String.Copy(a);
      string c = "hello";

      // compare string values for a constant and an instance: True
      Console.WriteLine(a == b);

      // compare string references;
// a is a constant but b is an instance: False
      Console.WriteLine((object)a == (object)b);

      // compare string references, both constants have the same value,
      // so string interning points to same reference: True
      Console.WriteLine((object)a == (object)c);
   }
}

Output

True
False
True
False
True

See Also

C# Operators | §7.9 Relational operators | != Operator


!= Operator

The inequality operator (!=) returns false if its operands are equal, true otherwise. Inequality operators are predefined for all types, including string and object. User-defined types can overload the != operator.

For predefined value types, the inequality operator (!=) returns true if the values of its operands are different, false otherwise. For reference types other than string, != returns true if its two operands refer to different objects. For the string type, != compares the values of the strings.

User-defined value types can overload the != operator (see operator). So can user-defined reference types, although by default != behaves as described above for both predefined and user-defined reference types. If != is overloaded, == must also be overloaded.

Example

using System;
class Test
{
   public static void Main()
   {
      // Numeric inequality:
      Console.WriteLine((2 + 2) != 4);

      // Reference equality: two objects, same boxed value
      object s = 1;
      object t = 1;
      Console.WriteLine(s != t);

      // String equality: same string value, same string objects
      string a = "hello";
      string b = "hello";

      // compare string values
      Console.WriteLine(a != b);

      // compare string references
      Console.WriteLine((object)a != (object)b);
   }
}

Output

False
True
False
False

See Also

C# Operators | §7.9 Relational operators | == Operator


<= Operator

All numeric and enumeration types define a "less than or equal" relational operator (<=) that returns true if the first operand is less than or equal to the second, false otherwise.

User-defined types can overload the <= operator (see operator). If <= is overloaded, >= must also be overloaded.

Example

using System;
class Test
{
   public static void Main()
   {
      Console.WriteLine(1 <= 1.1);
      Console.WriteLine(1.1 <= 1.1);
   }
}

Output

True
True

See Also

C# Operators | §7.9 Relational operators


>= Operator

All numeric and enumeration types define a "greater than or equal" relational operator (>=) that returns true if the first operand is greater than or equal to the second, false otherwise.

User-defined types can overload the >= operator (see operator). If >= is overloaded, <= must also be overloaded.

Example

using System;
class Test
{
   public static void Main()
   {
      Console.WriteLine(1.1 >= 1);
      Console.WriteLine(1.1 >= 1.1);
   }
}

Output

True
True

See Also

C# Operators | §7.9 Relational operators


+= Operator

An expression using the += assignment operator, such as

x += y

is equivalent to

x = x + y

except that x is only evaluated once. The meaning of the + operator is dependent on the types of x and y (addition for numeric operands, concatenation for string operands, and so forth).

The += operator cannot be overloaded directly, but user-defined types can overload the + operator (see operator).

Example

using System;
class Test
{
   public static void Main()
   {
      int a = 5;
      a += 6;
      Console.WriteLine(a);
      string s = "Micro";
      s += "soft";
      Console.WriteLine(s);
   }
}

Output

11
Microsoft

See Also

C# Operators | §7.13 Assignment operators


-= Operator

An expression using the -= assignment operator, such as

x -= y

is equivalent to

x = x - y

except that x is only evaluated once. The meaning of the - operator is dependent on the types of x and y (subtraction for numeric operands, delegate removal for delegate operands, and so forth).

The -= operator cannot be overloaded directly, but user-defined types can overload the - operator (see operator).

Example

using System;
class Test
{
   public static void Main()
   {
      int a = 5;
      a -= 6;
      Console.WriteLine(a);
   }
}

Output

-1

See Also

C# Operators | §7.13 Assignment operators


*= Operator

An expression using the *= assignment operator, such as

x *= y

is equivalent to

x = x * y

except that x is only evaluated once. The * operator is predefined for numeric types to perform multiplication.

The *= operator cannot be overloaded directly, but user-defined types can overload the * operator (see operator).

Example

using System;
class Test
{
   public static void Main()
   {
      int a = 5;
      a *= 6;
      Console.WriteLine(a);
   }
}

Output

30

See Also

C# Operators | §7.13 Assignment operators


/= Operator

An expression using the /= assignment operator, such as

x /= y

is equivalent to

x = x / y

except that x is only evaluated once. The / operator is predefined for numeric types to perform division.

The /= operator cannot be overloaded directly, but user-defined types can overload the / operator (see operator).

Example

using System;
class Test
{
   public static void Main()
   {
      int a = 5;
      a /= 6;
      Console.WriteLine(a);
      double b = 5;
      b /= 6;
      Console.WriteLine(b);
   }
}

Output

0
0.83333333333333337

See Also

C# Operators | §7.13 Assignment operators


%= Operator

An expression using the %= assignment operator, such as

x %= y

is equivalent to

x = x % y

except that x is only evaluated once. The % operator is predefined for numeric types to compute the remainder after division.

The %= operator cannot be overloaded directly, but user-defined types can overload the % operator (see operator).

Example

using System;
class Test
{
   public static void Main()
   {
      int a = 5;
      a %= 3;
      Console.WriteLine(a);
   }
}

Output

2

See Also

C# Operators | §7.13 Assignment operators


&= Operator

An expression using the &= assignment operator, such as

x &= y

is equivalent to

x = x & y

except that x is only evaluated once. The & operator performs a bitwise AND operation on integral operands and logical AND on bool operands.

The &= operator cannot be overloaded directly, but user-defined types can overload the & operator (see operator).

Example

using System;
class Test
{
   public static void Main()
   {
      int a = 0x0c;
      a &= 0x06;
      Console.WriteLine("0x{0:x8}", a);
      bool b = true;
      b &= false;
      Console.WriteLine(b);
   }
}

Output

0x00000004
False

See Also

C# Operators | §7.13 Assignment operators


|= Operator

An expression using the |= assignment operator, such as

x |= y

is equivalent to

x = x | y

except that x is only evaluated once. The | operator performs a bitwise OR operation on integral operands and logical OR on bool operands.

The |= operator cannot be overloaded directly, but user-defined types can overload the | operator (see operator).

Example

using System;
class Test
{
   public static void Main()
   {
      int a = 0x0c;
      a |= 0x06;
      Console.WriteLine("0x{0:x8}", a);
      bool b = true;
      b |= false;
      Console.WriteLine(b);
   }
}

Output

0x0000000e
True

See Also

C# Operators | §7.13 Assignment operators


^= Operator

An expression of the form

x ^= y

is evaluated as

x = x ^ y

except that x is only evaluated once. The ^ operator performs a bitwise exclusive-OR operation on integral operands and logical exclusive-OR on bool operands.

The |= operator cannot be overloaded directly, but user-defined types can overload the | operator (see operator).

Example

using System;
class Test
{
   public static void Main()
   {
      int a = 0x0c;
      a ^= 0x06;
      Console.WriteLine("0x{0:x8}", a);
      bool b = true;
      b ^= false;
      Console.WriteLine(b);
   }
}

Output

0x0000000a
True

See Also

C# Operators | §7.13 Assignment operators


<<= Operator

An expression of the form

x <<= y

is evaluated as

x = x << y

except that x is only evaluated once. The << operator shifts x left by an amount specified by y.

The <<= operator cannot be overloaded directly, but user-defined types can overload the << operator (see operator).

Example

using System;
class Test
{
   public static void Main()
   {
      int a = 1000;
      a <<= 4;
      Console.WriteLine(a);
   }
}

Output

16000

See Also

C# Operators | §7.13 Assignment operators


>>= Operator

An expression of the form

x >>= y

is evaluated as

x = x >> y

except that x is only evaluated once. The >> operator shifts x right by an amount specified by y.

The >>= operator cannot be overloaded directly, but user-defined types can overload the >> operator (see operator).

Example

using System;
class Test
{
   public static void Main()
   {
      int a = 1000;
      a >>= 4;
      Console.WriteLine(a);
   }
}

Output

62

See Also

C# Operators | §7.13 Assignment operators


-> Operator

The -> operator combines pointer dereferencing and member access.

An expression of the form

x->y

(where x is a pointer of type T* and y is a member of T) is equivalent to

(*x).y

The -> operator can be used only in unmanaged code.

The -> operator cannot be overloaded.

Example

using System;
struct Point
{
   public int x, y;
}

class Test
{
   public static void Main()
   {
      Point pt = new Point();
      unsafe
      {
         Point* pp = &pt;
         pp->x = 123;
         pp->y = 456;
      }

      Console.WriteLine ( "{0} {1}", pt.x, pt.y );
   }
}

Output

123 456

See Also

C# Operators


C# Attributes

This section contains an introduction to attributes and descriptions of the following reserved attributes.

AttributeUsage | Describes how a custom attribute class can be used.
Conditional | Marks a conditional method, a method whose execution depends on a specified preprocessing identifier.
Obsolete | Marks a program entity that should not be used.

See Also

§17. Attributes | Attributes Tutorial | Introduction to Attributes | §19. Interoperability | Platform Invoke Tutorial


Introduction to Attributes

C# provides a mechanism for defining declarative tags, called attributes, which you can place on certain entities in your source code to specify additional information. The information that attributes contain can be retrieved at run time through reflection. You can use predefined attributes or you can define your own custom attributes. This section provides a general introduction to attributes in C#; for more detailed information, refer to §17. Attributes.

  • Using Attributes
  • Global Attributes
  • Creating Custom Attributes
  • Retrieving Attribute Information

See Also

C# Attributes | §17. Attributes | Attributes Tutorial | Platform Invoke Tutorial


Using Attributes

Attributes can be placed on most any declaration (though a specific attribute might restrict the types of declarations on which it is valid). Syntactically, an attribute is specified by placing the name of the attribute, enclosed in square brackets, in front of the declaration of the entity to which it applies. For example, a class with the attribute DllImport (see §19.6 The DllImport attribute) is declared like this:

[DllImport] public class MyDllimportClass { ... }

Many attributes have parameters, which can be either positional (unnamed) or named. Any positional parameters must be specified in a certain order and cannot be omitted; named parameters are optional and can be specified in any order. Positional parameters are specified first. For example, these three attributes are equivalent:

[DllImport("user32.dll", SetLastError=false, ExactSpelling=false)]
[DllImport("user32.dll", ExactSpelling=false, SetLastError=false)]
[DllImport("user32.dll")]

The first parameter, the DLL name, is positional and always comes first; the others are named. In this case, both named parameters default to false, so they can be omitted (refer to the individual attribute's documentation for information on default parameter values).

More than one attribute can be placed on a declaration, either separately or within the same set of brackets:

bool AMethod([In][Out]ref double x);
bool AMethod([Out][In]ref double x);
bool AMethod([In,Out]ref double x);

Some attributes can be specified more than once for a given entity. An example of such a multiuse attribute is Conditional:

[Conditional("DEBUG"), Conditional("TEST1")] void TraceMethod() {...}

See Also

Introduction to Attributes | Global Attributes | Creating Custom Attributes | Retrieving Attribute Information


Global Attributes

Most attributes are attached to specific language elements, such as classes or methods; however, some attributes are global — they apply to an entire assembly or module. Assembly-level attributes are specified as follows:

[assembly: attribute-list]

Module-level attributes are specified as follows:

[module: attribute-list]

where:

attribute-list
A list of applicable global attributes.

Global attributes appear in the source code after any top-level using directives and before any type or namespace declarations. Global attributes can appear in multiple source files in a single compilation.

Note Assembly-level attributes can be specified only if an assembly manifest is being created.

The following .NET attributes let you modify the information in an assembly:

  • System.Reflection.AssemblyConfigurationAttribute
  • System.Runtime.CompilerServices.AssemblyCultureAttribute
  • System.Reflection.AssemblyDescriptionAttribute
  • System.Runtime.CompilerServices.AssemblyKeyFileAttribute
  • System.Runtime.CompilerServices.AssemblyKeyNameAttribute
  • System.Runtime.CompilerServices.AssemblyDelaySignAttribute
  • System.Reflection.AssemblyTitleAttribute
  • System.Runtime.CompilerServices.AssemblyVersionAttribute

If you do not want an assembly created for a compilation, use the compiler option /target:module.

Example

[assembly: System.CLSCompliant(true)]

See Also

Introduction to Attributes | Using Attributes | Creating Custom Attributes | Retrieving Attribute Information


Creating Custom Attributes

You can create your own custom attributes by defining an attribute class, a class that derives directly or indirectly from System.Attribute (which makes identifying attribute definitions in metadata fast and easy). Suppose you want to tag classes and structs with the name of the programmer who wrote the class or struct. You might define a custom Author attribute class:

using System;
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Struct)]
public class Author : Attribute
{
   public Author(string name) { this.name = name; version = 1.0; }
   public double version;
   string name;
}

The class name is the attribute's name, Author. It is derived from System.Attribute, so it is a custom attribute class. The constructor's parameters are the custom attribute's positional parameters (in this case, name), and any public read-write fields or properties are named parameters (in this case, version is the only named parameter). Note the use of the AttributeUsage attribute to make the Author attribute valid only on class and struct declarations.

You could use this new attribute as follows:

[Author("H. Ackerman", version=1.1)] class SomeClass{...}

AttributeUsage has a named parameter, AllowMultiple, with which you can make a custom attribute single-use or multiuse.

[AttributeUsage(AttributeTargets.Class|AttributeTargets.Struct,
   AllowMultiple=true)] // multiuse attribute
public class Author : Attribute
{
   ...
}
Note To prevent namespace collisions, all attribute names implicitly end with "Attribute"; in the preceding example, Author and AuthorAttribute could be used interchangeably. The same applies to AttributeUsage, which is defined in the .NET Framework as System.AttributeUsageAttribute.

See Also

Introduction to Attributes | Using Attributes | Global Attributes | Retrieving Attribute Information | AttributeUsage


Retrieving Attribute Information

The fact that you can define custom attributes and place them in your source code would be of little value without some way of retrieving that information and acting on it. Fortunately, C# has a reflection system that allows you to do just that. The key method is GetCustomAttributes, which returns an array of objects that are the run-time equivalents of the source code attributes.

An attribute specification such as:

[Author("H. Ackerman", version=1.1)] class AClass {...}

is conceptually equivalent to this:

Author anonymousAuthorObject = new Author("H. Ackerman");
anonymousAuthorObject.version = 1.1;

However, the code is not executed until AClass is queried for attributes. Calling GetCustomAttributes on AClass causes an Author object to be constructed and initialized as above. If the class has other attributes, other attribute objects are constructed similarly. GetCustomAttributes then returns the Author object and any other attribute objects in an array. You can then iterate over this array, determine what attributes were applied based on the type of each array element, and extract information from the attribute objects.

Example

Here is a complete example. A custom attribute is defined, applied to several entities, and retrieved via reflection.

using System;
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Struct,
   AllowMultiple=true)]
public class Author : Attribute
{
   public Author(string name)
   {
      this.name = name; version = 1.0;
   }
   public double version;
   string name;
   public string GetName()
   {
      return name;
   }
}

[Author("H. Ackerman")]
class FirstClass
{
   /*...*/
}

class SecondClass  // no Author attribute
{
   /*...*/
}

[Author("H. Ackerman"), Author("M. Knott", version=1.1)]
class Steerage
{
   /*...*/
}

class AuthorInfo
{
   public static void Main()
   {
      PrintAuthorInfo(typeof(FirstClass));
      PrintAuthorInfo(typeof(SecondClass));
      PrintAuthorInfo(typeof(Steerage));
   }
   public static void PrintAuthorInfo(Type t)
   {
      Console.WriteLine("Author information for {0}", t);
      Attribute[] attrs = Attribute.GetCustomAttributes(t);
      foreach(Attribute attr in attrs)
      {
         if (attr is Author)
         {
            Author a = (Author)attr;
            Console.WriteLine("   {0}, version {1:f}",
a.GetName(), a.version);
         }
      }
   }
}

Output

Author information for FirstClass
   H. Ackerman, version 1.00
Author information for SecondClass
Author information for Steerage
   H. Ackerman, version 1.00
   M. Knott, version 1.10

See Also

Introduction to Attributes | Using Attributes | Global Attributes | Creating Custom Attributes


AttributeUsage

Describes how a custom attribute class can be used.

[AttributeUsage(
 validon,
 AllowMultiple=allowmultiple,
 Inherited=inherited
)]

Parameters

validon
Specifies the language elements on which the attribute can be placed; a combination of AttributeTargets values. Default value is AttributeTargets.All.
allowmultiple (optional)
A bool; if true, the attribute is multiuse. Default is false (single-use).
inherited (optional)
A bool; if true, the attribute is inherited by derived classes. Default is false (not inherited).

Applies To

Class declarations.

Remarks

The AttributeUsage attribute is a single-use attribute. AttributeUsage is an alias for System.AttributeUsageAttribute.

Example

See the example in Retrieving Attribute Information.

See Also

C# Attributes | Attributes Tutorial


Conditional

Marks a conditional method whose execution depends on a specified preprocessing identifier.

[Conditional(
 conditionalSymbol
)]

Parameter

conditionalSymbol
A string representing the name of a preprocessing identifier.

Applies To

Method declarations.

Remarks

The Conditional attribute is a multiuse attribute. Conditional is an alias for System.Diagnostics.ConditionalAttribute.

Wherever a conditional method is called, the presence or absence of the preprocessing symbol specified by conditionalSymbol at the point of the call determines whether the call is included or omitted. If the symbol is defined, the call is included; otherwise, the call is omitted. Conditional methods provide a cleaner, more elegant alternative to enclosing the method call in #if conditionalSymbol...#endif preprocessor directives.

A conditional method must be a method in a class or struct declaration and must have a return type of void (for other restrictions, see §17.4.2 The Conditional attribute).

If a method has multiple Conditional attributes, a call to the method is included if at least one of the conditional symbols is defined (in other words, the symbols are logically ORed together).

[Conditional("A"), Conditional("B")] public static void IfAorB( )
{
   ...
}

To achieve the effect of logically ANDing symbols, you can define serial conditional methods:

[Conditional("A")] public static void IfAandB( )
{
   AandBPrivate( );
}
[Conditional("B")] static void AandBPrivate( )
{
/* Code to execute when both A and B are defined... */
}

Call IfAandB; if both A and B are defined, AandBPrivate will execute.

Example

#define DEBUG
using System;
using System.Diagnostics;
public class Trace
{
   [Conditional("DEBUG")] public static void Msg(string msg)
   {
      Console.WriteLine(msg);
   }
}
class Test
{
   static void A( )
   {
      Trace.Msg("Now in A.");
      B( );
   }
   static void B( )
   {
      Trace.Msg("Now in B.");
   }
   public static void Main( )
   {
      Trace.Msg("Now in Main.");
      A( );
      Console.WriteLine("Done.");
   }
}

Output

Now in Main.
Now in A.
Now in B.
Done.

Example

Same as above, but with first line omitted, or changed to

#undef DEBUG

Output

Done.

See Also

C# Attributes | Conditional Methods Tutorial


Obsolete

Marks a program entity that should not be used.

[Obsolete(
 message
)]
[Obsolete(
 message,
 iserror
)]

Parameters

message
A string; ideally, a human-readable explanation of why the item is obsolete and what to use instead.
iserror
A bool; if true, the compiler should treat the use of the item as an error. Default value is false (compiler generates a warning).

Applies To

Any declaration that allows attributes.

Remarks

The Obsolete attribute is a single-use attribute. Obsolete is an alias for System.ObsoleteAttribute.

When an entity marked Obsolete is used in a program, the compiler issues either an error or a warning (depending on iserror) and prints out message.

Example

using System;
public class MyClass
{
   [Obsolete("Don't use OldWay; use NewWay instead", true)]
   static void OldWay( ) { Console.WriteLine("Silly me!"); }
   static void NewWay( ) { Console.WriteLine("D'oh!"); }
   public static void Main( )
   {
      OldWay( );
   }
}

See Also

C# Attributes


C# Preprocessor Directives

This section discusses the C# language's preprocessor directives:

  • #if
  • #else
  • #elif
  • #endif
  • #define
  • #undef
  • #warning
  • #error
  • #line
  • #region
  • #endregion

While the compiler does not have a separate preprocessor, the directives described in this section are processed as if there was one; these directives are used to aid in conditional compilation. Unlike C and C++ directives, you cannot use these directives to create macros.

A preprocessor directive must be the only instruction on a line.

For more information, see §2.3 Pre-processing.

See Also

C# Programmer's Reference | Conditional Methods Tutorial


#if

#if lets you begin a conditional directive, testing a symbol or symbols to see if they evaluate to true. If they do evaluate to true, the compiler evaluates all the code between the #if and the next directive.

#if symbol [operator symbol]...

where:

symbol
The name of the symbol you want to test. You can also use true and false. symbol can be prefaced with the negation operator. For example, !true will evaluate to false.
operator (Optional)
You can use the following operators to evaluate multiple symbols:

== (equality)

!= (inequality)

&& (and)

|| (or)

You can group symbols and operators with parentheses.

Remarks

#if, along with the #else, #elif, #endif, #define, and #undef directives, lets you include or exclude code based on the condition of one or more symbols. This can be most useful when compiling code for a debug build or when compiling for a specific configuration.

A conditional directive beginning with a #if directive must explicitly be terminated with a #endif directive.

Example

#define DEBUG
#define VC_V6
using System;
public class MyClass
{
   public static void Main()
   {

      #if (DEBUG && !VC_V6)
         Console.WriteLine("DEBUG is defined");
      #elif (!DEBUG && VC_V6)
         Console.WriteLine("VC_V6 is defined");
      #elif (DEBUG && VC_V6)
         Console.WriteLine("DEBUG and VC_V6 are defined");
      #else
         Console.WriteLine("DEBUG and VC_V6 are not defined");
      #endif
   }
}

Output

DEBUG and VC_V6 are defined

See Also

C# Preprocessor Directives


#else

#else lets you create a compound conditional directive, such that, if none of the expressions in the preceding #if or (optional) #elif directives did not evaluate to true, the compiler will evaluate all code between #else and the subsequent #endif.

#else

Remarks

#endif must be the next preprocessor directive after #else.

Example

See #if for an example of how to use #else.

See Also

C# Preprocessor Directives


#elif

#elif lets you create a compound conditional directive. The #elif expression will be evaluated if neither the preceding #if nor any preceding (optional) #elif directive expressions evaluate to true. If a #elif expression evaluates to true, the compiler evaluates all the code between the #elif and the next directive.

#elif symbol [operator symbol]...

where:

symbol
The name of the symbol you want to test. You can also use true and false. symbol can be prefaced with the negation operator. For example, !true will evaluate to false.
operator (Optional)
You can use the following operators to evaluate multiple symbols:

== (equality)

!= (inequality)

&& (and)

|| (or)

You can group symbols and operators with parentheses.

Remarks

#elif is equivalent to using:

#else
#if

But using #elif is simpler because each #if requires a #endif, whereas a #elif can be used without a matching #endif.

Example

See #if for an example of how to use #elif.

See Also

C# Preprocessor Directives

p>© 1987-2000 Microsoft Corporation. All rights reserved.


#endif

#endif specifies the end of a conditional directive, which began with the #if directive.

#endif

Remarks

A conditional directive, beginning with a #if directive, must explicitly be terminated with a #endif directive.

Example

See #if for an example of how to use #endif.

See Also

C# Preprocessor Directives


#define

#define lets you define a symbol, such that, by using the symbol as the expression passed to the #if directive, the expression will evaluate to true.

#define symbol

where:

symbol
The name of the symbol to define.

Remarks

Symbols can be used to specify conditions for compilation. You can test for the symbol with either #if or #elif. You can also use the conditional attribute to perform conditional compilation.

You can define a symbol, but you cannot assign a value to a symbol. The #define directive must appear in the file before you use any instructions that are not also directives.

You can also define a symbol with the /define compiler option. You can undefine a symbol with #undef.

A symbol that you define with /define or with #define does not conflict with a variable of the same name. That is, a variable name should not be passed to a preprocessor directive and a symbol can only be evaluated by a preprocessor directive.

The scope of a symbol created with #define is the file in which it was defined.

Example

See #if for an example of how to use #define.

See Also

C# Preprocessor Directives | Conditional Methods Tutorial


#undef

#undef lets you undefine a symbol, such that, by using the symbol as the expression in a #if directive, the expression will evaluate to false.

#undef symbol

where:

symbol
The name of the symbol you want to undefine.

Remarks

A symbol can be defined either with the #define directive or the /define compiler option. The #undef directive must appear in the file before you use any statements that are not also directives.

Example

// compile with /D:DEBUG
#undef DEBUG
using System;
public class MyClass
{
   public static void Main()
   {
      #if DEBUG
         Console.WriteLine("DEBUG is defined");
      #else
         Console.WriteLine("DEBUG is not defined");
      #endif
   }
}

Output

DEBUG is not defined

See Also

C# Preprocessor Directives


#warning

#warning lets you generate a level one warning from a specific location in your code.

#warning text

where:

text
The text of the warning that should appear in the compiler's output.

Remarks

A common use of #warning is in a conditional directive. It is also possible to generate a user-defined error with #error.

Example

#define DEBUG
public class MyClass
{
   public static void Main()
   {
      #if DEBUG
      #warning DEBUG is defined
   #endif
   }
}

See Also

C# Preprocessor Directives


#error

#error lets you generate an error from a specific location in your code.

#error text

where:

text
The text of the error that should appear in the compiler's output.

Remarks

A common use of #error is in a conditional directive. It is also possible to generate a user-defined warning with #warning.

Example

#define DEBUG
public class MyClass
{
   public static void Main()
   {
      #if DEBUG
      #error DEBUG is defined
   #endif
   }
}

See Also

C# Preprocessor Directives


#line

#line lets you modify the compiler's line number and (optionally) the file name output for errors and warnings.

#line number ["file_name"]

where:

number
The number you want to specify for a line in a source code file.
"file_name" (optional)
The file name you want to appear in the compiler output. By default, the actual name of the source code file is used. The file name must be in double quotation marks ("").

Remarks

#line might be used by an automated, intermediate step in the build process. For example, if the intermediate step removed lines from the original source code file, but if you still wanted the compiler to generate output based on the original line numbering in the file, you could remove lines and then simulate the original line numbering with #line.

A source code file can have any number of #line directives.

Example

using System;
public class MyClass
{
   public static void Main()
   {
      #line 100 "abc.sc"   // change file name in the compiler output
      intt i;   // error will be reported on line 101
   }
}

See Also

C# Preprocessor Directives


#region

#region lets you specify a block of code that you can expand or collapse when using the outlining feature of the Visual Studio Code Editor.

#region name

where:

name
The name you want to give to the region, which will appear in the Visual Studio Code Editor.

Remarks

A #region block must be terminated with a #endregion directive.

A #region block cannot overlap with a #if block. However, a #region block can be nested in a #if block, and a #if block can be nested in a #region block.

Example

#region MyClass definition
public class MyClass
{
   public static void Main()
   {
   }
}
#endregion

See Also

C# Preprocessor Directives


#endregion

#endregion marks the end of a #region block.

#endregion

Example

See #region for an example of how to use #endregion.

See Also

C# Preprocessor Directives


C# Language Features

This section of the C# Programmer's Reference provides information about the following language features:

  • Arrays
  • Main
  • Properties
  • Indexers
  • XML Documentation

See Also

C# Programmer's Reference | C# Tutorials


Arrays

An array is a data structure that contains a number of variables called the elements of the array. The array elements are accessed through computed indexes. C# arrays are zero indexed; that is, the array indexes start at zero. All the array elements must be of the same type, which is called the element type of the array. Array elements can be of any type, including an array type. An array can be a single-dimensional array, or a multidimensional array. Array types are reference types derived from the abstract base type System.Array.

The following topics are discussed in this section:

  • Single-dimensional Arrays
  • Multidimensional Arrays
  • Passing Arrays Using ref and out

See Also

Reference Types| §12. Arrays | Arrays Tutorial


Single-dimensional Arrays

You can declare an array of five integers as in the following example:

int[] myArray = new int [5];

This array contains the elements from myArray[0] to myArray[4]. The new operator is used to create the array and initialize the array elements to their default values. In this example, all the array elements are initialized to zero.

An array that stores string elements can be declared in the same way. For example:

string[] myStringArray = new string[6];

Array Initialization

It is possible to initialize an array upon declaration, in which case, the rank specifier is not needed because it is already supplied by the number of elements in the initialization list. For example:

int[] myArray = new int[] {1, 3, 5, 7, 9};

A string array can be initialized in the same way. The following is a declaration of a string array where each array element is initialized by a name of a day:

string[] weekDays = new string[]
               {"Sun","Sat","Mon","Tue","Wed","Thu","Fri"};

When you initialize an array upon declaration, it is possible to use the following shortcuts:

int[] myArray = {1, 3, 5, 7, 9};
string[] weekDays = {"Sun","Sat","Mon","Tue","Wed","Thu","Fri"};

It is possible to declare an array variable without initialization, but you must use the new operator when you assign an array to this variable. For example:

int[] myArray;
myArray = new int[] {1, 3, 5, 7, 9};   // OK
myArray = {1, 3, 5, 7, 9};   // Error

Passing Arrays as Parameters

You can pass an initialized array to a method. For example:

PrintArray(myArray);

You can also initialize and pass a new array in one step. For example:

PrintArray(new int[] {1, 3, 5, 7, 9});

Example

In the following example, a string array is initialized and passed as a parameter to the PrintArray method, where its elements are displayed:

// Single-dimensional arrays
using System;
public class ArrayClass
{
   static void PrintArray(string[] w)
   {
      for (int i=0; i < w.Length; i++)
         Console.Write(w[i] + " ");
   }

   public static void Main()
   {
      // Declare and initialize an array:
      string[] WeekDays = new string []
         {"Sun","Sat","Mon","Tue","Wed","Thu","Fri"};

      // Pass the array as a parameter:
      PrintArray(WeekDays);
   }
}

Output

Sun Sat Mon Tue Wed Thu Fri

Example

This example builds an array, myArray, whose elements are arrays. Each one of the array elements has a different size.

// Array of arrays
using System;
public class ArrayTest
{
   public static void Main()
   {
      // Declare the array of two elements:
      int[][] myArray = new int[2][];

      // Initialize the elements:
      myArray[0] = new int[5] {1,3,5,7,9};
      myArray[1] = new int[4] {2,4,6,8};

      // Display the array elements:
      for (int i=0; i < 2; i++)
      {
         Console.Write("Element({0}): ", i);

         for (int j=0; j < myArray[i].Length; j++)
            Console.Write("{0} ", myArray[i][j]);

         Console.WriteLine();
      }
   }
}

Output

Element(0): 1 3 5 7 9

Element(1): 2 4 6 8

See Also

Arrays | Multidimensional Arrays


Multidimensional Arrays

The following declaration creates a two-dimensional array of four rows and two columns:

int[,] myArray = new int[4, 2];

Array Initialization

You can initialize the array upon declaration as shown in the following example:

int[,] myArray = new  int[,] {{1,2} , {3,4} , {5,6} , {7,8}};

You can also initialize the array without specifying the rank:

int[,] myArray = {{1,2} , {3,4} , {5,6} , {7,8}};

If you choose to declare an array variable without initialization, you must use the new operator to assign an array to the variable. For example:

int[,] myArray;
myArray = new int[,]{{1,2} , {3,4} , {5,6} , {7,8}};   // OK
myArray = {{1,2} , {3,4} , {5,6} , {7,8}};   // Error

Passing Arrays as Parameters

You can pass an initialized array to a method. For example:

PrintArray(myArray);

You can also initialize and pass a new array in one step. For example:

PrintArray(new int[,]{{1,2} , {3,4} , {5,6} , {7,8}});

Example

In this example, a two-dimensional array is initialized and passed to the PrintArray method, where its elements are displayed.

// Two-dimensional arrays
using System;
public class ArrayClass
{
   static void PrintArray(int[,] w)
   {
      // Display the array elements:
      for (int i=0; i < 4; i++)
         for (int j=0; j < 2; j++)
            Console.WriteLine("Element({0},{1})={2}", i, j, w[i,j]);
   }

   public static void Main()
   {
      // Pass the array as a parameter:
      PrintArray(new int[,]{{1,2} , {3,4} , {5,6} , {7,8}});
   }
}

Output

Element(0,0)=1
Element(0,1)=2
Element(1,0)=3
Element(1,1)=4
Element(2,0)=5
Element(2,1)=6
Element(3,0)=7
Element(3,1)=8

See Also

Arrays | Single-dimensional Arrays


Passing Arrays Using ref and out

Like all out parameters, an out parameter of an array type must be assigned before it is used; that is, it must be assigned by the callee. For example:

public static void MyMethod(out int[] arr) {
   arr = new int[10];   // definite assignment of arr
}

Like all ref parameters, a ref parameter of an array type must be definitely assigned by the caller. Therefore, there is no need to be definitely assigned by the callee. A ref parameter of an array type may be altered as a result of the call. For example, the array can be assigned the null value or can be initialized to a different array. For example:

public static void MyMethod(ref int[] arr) {
   arr = new int[10];   // arr initialized to a different array
}

The following two examples demonstrate the difference between out and ref when used in passing arrays to methods.

Example

In this example, the array myArray is declared in the caller (the Main method), and initialized in the FillArray method. Then, the array elements are returned to the caller and displayed.

// Using out to pass the array to a method.
using System;
class TestOut
{
   static public void FillArray(out int[] myArray)
   {
      // Initialize the array:
      myArray = new int[5] {1, 2, 3, 4, 5};
   }

   static public void Main()
   {
      int[] myArray; // Initialization is not required

      // Pass the array to the callee using out:
      FillArray(out myArray);

      // Display the array elements:
      Console.WriteLine("Array elements are:");
      for (int i=0; i < myArray.Length; i++)
         Console.WriteLine(myArray[i]);
   }
}

Output

Array elements are:
1
2
3
4
5

Example

In this example, the array myArray is initialized in the caller (the Main method), and passed to the FillArray method by using the ref parameter. Some of the array elements are updated in the FillArray method. Then, the array elements are returned to the caller and displayed.

// Using ref to pass the array to a method
using System;
class TestRef
{
   public static void FillArray(ref int[] arr)
   {
      // Create the array on demand:
      if (arr == null)
         arr = new int[10];
      // Otherwise fill the array:
      arr[0] = 123;
      arr[4] = 1024;
   }

   static public void Main ()
   {
      // Initialize the array:
      int[] myArray = {1,2,3,4,5};

      // Pass the array using ref:
      FillArray(ref myArray);

      // Display the updated array:
      Console.WriteLine("Array elements are:");
      for (int i = 0; i < myArray.Length; i++)
         Console.WriteLine(myArray[i]);
   }
}

Output

Array elements are:
123
2
3
4
1024

See Also

Arrays


Main

The Main method is the entry point of your program, where the program control starts and ends. It is declared inside a class or struct. It must be static. It can either be void or return an int. The Main method is where you create objects and invoke other methods. The Main method can be written without parameters or with parameters. The latter form allows your program to read command-line arguments.

See the following topics for more information and examples on using the Main method:

  • Return Values
  • Command-line Arguments

See Also

C# Language Features


Return Values

The Main method can be of the type void:

static void Main()
{
}

It can also return an int:

static int Main()
{
   return 0;
}

Example

In this example, the program contains two classes, Factorial and MainClass. The Main method, which resides in the MainClass class, is used to read a number from the keyboard, invoke the Fac() method from the Factorial class, and calculate and display the factorial of the input number.

// The Main method
using System;
public class Factorial
{
   public static long Fac(long i)
   {
      return ((i <= 1) ? 1 : (i * Fac(i-1)));
   }
}

class MyClass
{
   public static void Main()
   {
      // Read a string from the keyboard:
      Console.Write("Enter an integer: ");

      string s = Console.ReadLine();

      // Convert the string to long:
      long num = Int64.FromString(s);

      Console.WriteLine("The Factorial of {0} is {1}.",
                         num, Factorial.Fac(num));
   }
}

Sample Run

Enter an integer: 5
The Factorial of 5 is 120.

See Also

Command-line Arguments | Main


Command-line Arguments

The Main method can use arguments, in which case, it takes one of the following forms:

static int Main(string[] args)
static void Main(string[] args)

The parameter of the Main method is a string array that represents the command-line arguments. Usually you check for the existence of the arguments by testing the Length property, for example:

if (args.Length == 0)
{
   Console.WriteLine("Please enter a numeric argument.");
   return 1;
}

You can also convert the string arguments to numeric types by using the Parse() method. For example, the following statement converts the string to a long number by using the Parse() method on the Int64 class (for more information on System classes, see the Reference section of the .NET SDK):

long num = Int64.Parse(args[0]);

It is also possible to use the C# type long, which aliases Int64:

long num = long.Parse(args[0]);

Example

In this example, the program takes one argument at run time, converts the argument to a long number, and calculates the factorial of the number. If no arguments are supplied, the program issues a message that explains the correct usage of the program.

// Command line arguments
using System;
public class Factorial
{
   public static long Fac(long i)
   {
      return ((i <= 1) ? 1 : (i * Fac(i-1)));
   }
}

class MainClass
{
   public static int Main(string[] args)
   {
      // Test if input arguments were supplied:
      if (args.Length == 0)
      {
         Console.WriteLine("Please enter a numeric argument.");
         Console.WriteLine("Usage: Factorial <num>");
         return 1;
      }

      // Convert the input arguments to numbers:
      long num = long.Parse(args[0]);
      Console.WriteLine("The Factorial of {0} is {1}.",
                        num, Factorial.Fac(num));
      return 0;
   }
}

Sample Runs

The following are two sample runs of the program assuming that the program name is Factorial.exe.

Run #1:

Factorial 10
The Factorial of 10 is 3628800.

Run #2:

Factorial
Please enter a numeric argument.
Usage: Factorial <num>

For more examples on using command-line arguments, see the example in Creating and Using C# DLLs.

See Also

Main | Return Values | General Structure of a C# Program | Command Line Parameters Tutorial


Properties

Properties are named members of classes, structs, and interfaces. They provide a flexible mechanism to read, write, or compute the values of private fields through accessors.

In this section, the following topics are introduced:

  • Property Declaration
  • Accessors
  • Interface Properties

See Also

Indexers | Properties Tutorial


Property Declaration

Properties are an extension of fields and are accessed using the same syntax. Unlike fields, properties do not designate storage locations. Instead, properties have accessors that read, write, or compute their values.

Property declaration takes one of the following forms:

[attributes] [modifiers] type identifier {accessor declaration}
[attributes] [modifiers] type interface-type.identifier {accessor declaration}

where:

attributes (Optional)
Additional declarative information. For more information on attributes and attribute classes, see C# Attributes.
modifiers (Optional)
The allowed modifiers are new, static, virtual, abstract, override, and a valid combination of the four access modifiers.
type
The property type, which must be at least as accessible as the property itself. For more information on accessibility levels, see Access Modifiers.
identifier
The property name. For more information on interface member implementation, see interface.
accessor-declaration
Declaration of the property accessors, which are used to read and write the property.
interface-type
The interface in a fully-qualified property name. See Interface Properties.

Remarks

  • Unlike fields, properties are not classified as variables. Therefore, it is not possible to pass a property as a ref or out parameter.
  • A property declared using the static modifier is classified as a static property; otherwise, it is classified as an instance property. Like other static members, a static property is not associated with a specific instance and cannot be referenced through an instance. Instead, it is associated with the type and can only be referenced through the type name. For example, in the following statements:
    Button okButton = new Button();
// Using an instance property:
string s = okButton.Caption;

    the Caption property is associated with the instance okButton. If Caption is declared as a static property, the class name (Button) must be used instead:

    // Using a static property:
string s = Button.Caption;
  • An instance of a class can be accessed using this in the accessors of an instance property, but it is an error to use this in the accessors of a static property.
  • It is an error to use a virtual, abstract, or override modifier on an accessor of a static property.

Example

This example demonstrates instance, static, and read-only properties. It accepts the name of the employee from the keyboard, increments numberOfEmployees by 1, and displays the Employee name and number.

// Properties
using System;
public class Employee
{
   public static int numberOfEmployees;
   private static int counter;
   private string name;

   // A read-write instance property:
   public string Name
   {
      get
      {
         return name;
      }
      set
      {
         name = value;
      }
   }

   // A read-only static property:
   public static int Counter
   {
      get
      {
         return counter;
      }
   }

   // Constructor:
   public Employee()
   {
      // Calculate the employee's number:
      counter = ++counter + numberOfEmployees;
   }
}

public class MainClass
{
   public static void Main()
   {
      Employee.numberOfEmployees = 100;
      Employee e1 = new Employee();
      e1.Name = "Claude Vige";
      Console.WriteLine("Employee number: {0}", Employee.Counter);
      Console.WriteLine("Employee name: {0}", e1.Name);
   }
}

Output

Employee number: 101
Employee name: Claude Vige

See Also

Accessors | Indexers | Properties | Properties Tutorial


Accessors

The accessor of a property contains the executable statements associated with getting (reading or computing) or setting (writing) the property. The accessor declarations can contain a get accessor, a set accessor, or both. The declarations take the following forms:

set {accessor-body}
get {accessor-body}

where:

accessor-body
The block that contains the statements to be executed when the accessor is invoked.

The get Accessor

The body of the get accessor is similar to that of a method. It must return a value of the property type. The execution of the get accessor is equivalent to reading the value of the field. The following is a get accessor that returns the value of a private field name:

private string name;   // the name field
public string Name   // the Name property
{
   get
   {
      return name;
   }
}

When you reference the property, except as the target of an assignment, the get accessor is invoked to read the value of the property. For example:

Employee e1 = new Employee();
...
Console.Write(e1.Name);   // The get accessor is invoked here

The get accessor must terminate in a return or throw statement, and control cannot flow off the accessor body.

The set Accessor

The set accessor is similar to a method that returns void. It uses an implicit parameter called value, whose type is the type of the property. In the following example, a set accessor is added to the Name property:

public string Name
{
   get
   {
      return name;
   }
   set
   {
      name = value;
   }
}

When you assign a value to the property, the set accessor is invoked with an argument that provides the new value. For example:

e1.Name = "Joe";   // The set accessor is invoked here

It is an error to use the implicit parameter name (value) for a local variable declaration in a set accessor.

Remarks

A property is classified according to the accessors used as follows:

  • A property with a get accessor only is called a read-only property. You cannot assign a value to a read-only property.
  • A property with a set accessor only is called a write-only property. You cannot reference a write-only property except as a target of an assignment.
  • A property with both get and set accessors is a read-write property.

In a property declaration, both the get and set accessors must be declared inside the body of the property.

It is a bad programming style to change the state of the object by using the get accessor. For example, the following accessor produces the side effect of changing the state of the object each time the number field is accessed.

public int Number
{
   get
   {
      return number++;   // Don't do this
   }
}

The get accessor can either be used to return the field value or to compute it and return it. For example:

public string Name
{
   get
   {
      return name != null ? name : "NA";
   }
}

In the preceding code segment, if you don't assign a value to the Name property, it will return the value NA.

Example

This example demonstrates how to access a property in a base class that is hidden by another property with the same name in a derived class.

// Property hiding
using System;
public class BaseClass
{
   private string name;
   public string Name
   {
      get
      {
         return name;
      }
      set
      {
         name = value;
      }
   }
}

public class DerivedClass : BaseClass
{
   private string name;
   public new string Name   // Notice the use of the new modifier
   {
      get
      {
         return name;
      }
      set
      {
         name = value;
      }
   }
}

public class MainClass
{
   public static void Main()
   {
      DerivedClass d1 = new DerivedClass();
      d1.Name = "John";  // Derived class property
      Console.WriteLine("Name in the derived class is: {0}",d1.Name);
      ((BaseClass)d1).Name = "Mary"; // Base class property
      Console.WriteLine("Name in the base class is: {0}",
         ((BaseClass)d1).Name);
   }
}

Output

Name in the derived class is: John
Name in the base class is: Mary

The following are important points shown in the preceding example:

The property Name in the derived class hides the property Name in the base class. In such a case, the new modifier is used in the declaration of the property in the derived class:

   public new string Name
   {
   ...

The cast (BaseClass) is used to access the hidden property in the base class:

   ((BaseClass)d1).Name = "Mary";

For more information on hiding members, see the new modifier.

Example

In this example two classes, Cube and Square, implement an abstract class, Shape, and override its abstract Area property. Note the use of the override modifier on the properties. The program accepts the side as an input and calculates the areas for the square and cube. It also accepts the area as an input and calculates the corresponding side for the square and cube.

// Overriding properties
using System;
abstract class Shape
{
   public abstract double Area
   {
      get;
      set;
   }
}

class Square: Shape
{
      public double side;

      // Constructor:
      public Square(double s)
      {
         side = s;
      }

   // The Area property
   public override double Area
   {
      get
      {
         return side*side ;
      }
      set
      {
         // Given the area, compute the side
         side = Math.Sqrt(value);
      }
   }
}

class Cube: Shape {
      public double side;

      // Constructor:
      public Cube(double s)
      {
         side = s;
      }

      // The Area property
      public override double Area {
      get
      {
         return 6*side*side;
      }
      set
      {
         // Given the area, compute the side
         side = Math.Sqrt(value/6);
      }
   }
}

public class MainClass
{
   public static void Main()
   {
      // Input the side:
      Console.Write("Enter the side: ");
      string sideString = Console.ReadLine();
      double side = double.FromString(sideString);

      // Compute areas:
      Square s = new Square(side);
      Cube c = new Cube(side);

      // Display results:
      Console.WriteLine("Area of a square = {0}", double.Format(s.Area, "0.00"));
      Console.WriteLine("Area of a cube = {0}", double.Format(c.Area, "0.00"));

      // Input the area:
      Console.Write("Enter the area: ");
      string areaString = Console.ReadLine();
      double area = double.FromString(areaString);

      // Compute areas:
      s.Area = area;
      c.Area = area;

      // Display results:
      Console.WriteLine("Side of a square = {0}", double.Format(s.side, "0.00"));
      Console.WriteLine("Side of a cube = {0}", double.Format(c.side, "0.00"));
   }
}

Sample Run

Enter the side: 4
Area of a square = 16.00
Area of a cube = 96.00
Enter the area: 24
Side of a square = 4.90
Side of a cube = 2.00

See Also

Properties | virtual | override | abstract | base | Indexers | new modifier | Properties Tutorial | Indexers Tutorial


Interface Properties

Properties can be declared on interfaces. The declaration takes the following form:

[attributes] [new] type identifier {interface-accessors}

where:

attributes (Optional)
Same as properties on classes.
type
Same as properties on classes.
identifier
The property name.
interface-accessors
The property accessors. See the Remarks below.

Remarks

The body of the accessor of an interface property consists of a semicolon.

Thus, the purpose of the accessors is to indicate whether the property is read-write, read-only, or write-only.

The following is an example of an interface indexer accessor:

public interface IMyInterface {
   // Property declaration:
   string Name {
      get;
      set;
   }
}

Example

In this example, the interface IEmployee has a read-write property, Name, and a read-only property, Counter. The class Employee implements the IEmployee interface and uses these two properties. The program reads the name of a new employee and the current number of employees and displays the employee name and the computed employee number.

// Interface Properties
using System;
interface IEmployee
{
   string Name
   {
      get;
      set;
   }

   int Counter
   {
      get;
   }
}

public class Employee: IEmployee
{
   public static int numberOfEmployees;
   private int counter;
   private string name;
   // Read-write instance property:
   public string Name
   {
      get
      {
         return name;
      }
      set
      {
         name = value;
      }
   }
   // Read-only instance property:
   public int Counter
   {
      get
      {
         return counter;
      }
   }
   // Constructor:
   public Employee()
   {
      counter = ++counter + numberOfEmployees;
   }
}

public class MainClass
{
   public static void Main()
   {
      Console.Write("Enter number of employees: ");
      string s = Console.ReadLine();
      Employee.numberOfEmployees = int.FromString(s);
      Employee e1 = new Employee();
      Console.Write("Enter the name of the new employee: ");
      e1.Name = Console.ReadLine();
      Console.WriteLine("The employee information:");
      Console.WriteLine("Employee number: {0}", e1.Counter);
      Console.WriteLine("Employee name: {0}", e1.Name);
   }
}

Sample Run

Enter number of employees: 201
Enter the name of the new employee: Vivian Nicola
The employee information:
Employee number: 202
Employee name: Vivian Nicola

In the preceding example, you could use the fully qualified name of the property, which references the interface in which the member is declared. For example:

public string IEmployee.Name
{
}

However, the fully qualified name is only needed to avoid ambiguity when the class is implementing more than one interface with the same property signature. For example, if the class Employee is implementing two interfaces ICitizen and IEmployee and both interfaces have the Name property, the explicit interface member implementation will be necessary. That is, the following property declaration:

public string IEmployee.Name
{
}

implements the Name property on the IEmployee interface, while the following declaration:

public string ICitizen.Name
{
}

implements the Name property on the ICitizen interface.

For more information, see §13.4.1 Explicit interface member implementations.

See Also

Properties | Accessors | Comparison Between Properties and Indexers


Indexers

Indexers permit instances of a class or struct to be indexed in the same way as arrays. Indexers are similar to properties except that their accessor take parameters.

In this section, the following topics are introduced:

  • Indexer Declaration
  • Comparison Between Properties and Indexers
  • Interface Indexers

For information on associated arrays, see the .NET Framework Hashtable Class.

See Also

Properties | Indexers Tutorial


Indexer Declaration

Indexers allow you to index a class or a struct instance in the same way as an array. To declare an indexer, use the following declaration:

[attributes] [modifiers] indexer-declarator {accessor-declarations}

The indexer-declarator takes one of the forms:

type this [formal-index-parameter-list]
type interface-type.this [formal-index-parameter-list]

The formal-index-parameter takes the form:

[attributes] type identifier

where:

attributes (Optional)
Additional declarative information. For more information on attributes and attribute classes, see C# Attributes.
modifiers (Optional)
Allowed modifiers are new and a valid combination of the four access modifiers.
indexer-declarator
Includes the type of the element introduced by the indexer, this, and the formal-index-parameter-list. If the indexer is an explicit interface member implementation, the interface-type is included.
formal-index-parameter-list
Specifies the parameters of the indexer. The parameter includes optional attributes, the index type, and the index identifier. At least one parameter must be specified. The parameters out and ref are not allowed.
accessor-declarations
The indexer accessors, which specify the executable statements associated with reading and writing indexer elements.

The get Accessor

The get accessor body of an indexer is similar to a method body. It returns the type of the indexer. The get accessor uses the same formal-index-parameter-list as the indexer. For example:

get
{
   return myArray[index];
}

The set Accessor

The set accessor body of an indexer is similar to a method body. It uses the same formal-index-parameter-list as the indexer, in addition to the value implicit parameter. For example:

set
{
   myArray[index] = value;
}

Remarks

The type of an indexer and each of the types referenced in the formal-index-parameter-list must be at least as accessible as the indexer itself. For more information on accessibility levels, see Access Modifiers.

The signature of an indexer consists of the number and types of its formal parameters. It does not include the indexer type or the names of the formal parameters.

If you declare more than one indexer in the same class, they must have different signatures.

An indexer value is not classified as a variable; therefore, it is not possible to pass an indexer value as a ref or out parameter.

In order to provide the indexer a name that other languages can use for the default indexed property, use a name attribute in the declaration. For example:

   [name ("MyItem")]
   public int this [int index]   // indexer declaration
   {
}

This indexer will have the name MyItem. Without providing the name attribute, the default name will be Item.

Example

This example declares a private array field myArray and an indexer. Using the indexer allows direct access to the instance b[i]. The alternative to using the indexer is to declare the array as a public member and access its members, myArray[i], directly.

// Indexers
using System;
class IndexerClass
{
   private int [] myArray = new int[100];
   public int this [int index]   // indexer declaration
   {
      get
      {
         // Check the index limits
         if (index < 0 || index >= 100)
            return 0;
         else
            return myArray[index];
      }
      set
      {
         if (!(index < 0 || index >= 100))
            myArray[index] = value;
      }
   }
}

public class MainClass
{
   public static void Main()
   {
      IndexerClass b = new IndexerClass();
      // call the indexer to initialize the elements #3 and #5:
      b[3] = 256;
      b[5] = 1024;
      for (int i=0; i<=10; i++)
      {
         Console.WriteLine("Element #{0} = {1}", i, b[i]);
      }
   }
}

Output

Element #0 = 0
Element #1 = 0
Element #2 = 0
Element #3 = 256
Element #4 = 0
Element #5 = 1024
Element #6 = 0
Element #7 = 0
Element #8 = 0
Element #9 = 0
Element #10 = 0

Notice that when an indexer's access is evaluated (for example, in a Console.Write statement) the get accessor is invoked. Therefore, if no get accessor exists, a compile-time error occurs.

For an additional example, see the example in §10.8 Indexers.

See Also

Properties | Accessors | Indexers | Indexers Tutorial


Comparison Between Properties and Indexers

Indexers are similar to properties. Except for the differences shown in the following table, all the rules defined for property accessors apply to indexer accessors as well.

Property | Indexer
Identified by its name. | Identified by its signature.
Accessed through a simple name or a member access. | Accessed through an element access.
Can be a static or an instance member. | Must be an instance member.
A get accessor of a property has no parameters. | A get accessor of an indexer has the same formal parameter list as the indexer.
A set accessor of a property contains the implicit value parameter. | A set accessor of an indexer has the same formal parameter list as the indexer, in addition to the value parameter.

See Also

Properties | Accessors | Indexers


Interface Indexers

Indexers can be declared on interfaces. The declaration takes the following form:

[attributes] [new] type this [formal-index-parameter-list] {interface-accessors}

where:

attributes (Optional)
Same as indexers on classes.
type
Same as indexers on classes.
formal-index-parameter-list
Same as indexers on classes.
interface-accessors
The indexer accessors. See the Remarks below.

Remarks

Accessors of interface indexers differ from the accessors of class indexers as follows:

  • Interface accessors don't use modifiers.
  • The body of the accessor consists of a semicolon.

Thus, the purpose of the accessors is to indicate whether the indexer is read-write, read-only, or write-only.

The following is an example of an interface indexer accessor:

public interface IMyInterface
{
   ...
   // Indexer declaration:
   string this[int index]
{
      get;
      set;
   }
}

The signature of an indexer must differ from the signatures of all other indexers declared in the same interface.

Example

The following example demonstrates implementation of interface indexers.

using System;
// Indexer on an interface:
public interface IMyInterface
{
   // indexer declaration:
   int this[int index]
   {
      get;
      set;
   }
}

// Implementing the interface:
class IndexerClass
{
   private int [] myArray = new int[100];
   public int this [int index]   // indexer declaration
   {
      get
      {
         // Check the index limits
         if (index < 0 || index >= 100)
            return 0;
         else
            return myArray[index];
      }
      set
      {
         if (!(index < 0 || index >= 100))
            myArray[index] = value;
      }
   }
}

public class MainClass
{
   public static void Main()
   {
      IndexerClass b = new IndexerClass();
      // call the indexer to initialize the elements #3 and #5:
      b[2] = 4;
      b[5] = 32;
      for (int i=0; i<=10; i++)
      {
         Console.WriteLine("Element #{0} = {1}", i, b[i]);
      }
   }
}

Output

Element #0 = 0
Element #1 = 0
Element #2 = 4
Element #3 = 0
Element #4 = 0
Element #5 = 32
Element #6 = 0
Element #7 = 0
Element #8 = 0
Element #9 = 0
Element #10 = 0

In the preceding example, you could use the explicit interface member implementation by using the fully qualified name of the interface member. For example:

   public string IMyInterface.this
{
}

However, the fully qualified name is only needed to avoid ambiguity when the class is implementing more than one interface with the same indexer signature. For example, if the class Employee is implementing two interfaces, ICitizen and IEmployee, and both interfaces have the same indexer signature, the explicit interface member implementation will be necessary. That is, the following indexer declaration:

   public string IEmployee.this
{
}

implements the indexer on the IEmployee interface, while the following declaration:

   public string ICitizen.this
{
}

implements the indexer on the ICitizen interface.

For more information, see §13.4.1 Explicit interface member implementations.

See Also

interface | Indexers


XML Documentation

In C# you can document the code you write using XML. C# is the only programming language in Visual Studio.NET 7.0 with this feature. For details on creating an XML file with documentation comments, see the following topics.

For information about | See
The C# compiler option to use | /doc (Process Documentation Comments)
Tags you can use to provide commonly used functionality in documentation | Tags for Documentation Comments
The ID strings that the compiler produces to identify the constructs in your code | Processing the XML File
How to document a type | XML Documentation Tutorial
Links to information about XML as it relates to Visual Studio feature areas | XML in Visual Studio

See Commenting Code for information on how Visual Studio will display a report on the structure of your code, to include XML Documentation comments.

See Also

C# Language Features


Tags for Documentation Comments

The C# compiler will process documentation comments in your code to an XML file. Processing the XML file to create documentation is a detail that needs to be implemented at your site.

Tags are processed on code constructs such as types and type members.

Note Tags are not processed on namespaces.

The compiler will process any tag that is valid XML. The following tags provide commonly used functionality in user documentation:

<c> | <param> 1 | <see> 1
<code> | <paramref> 1 | <seealso> 1
<example> | <permission> 1 | <summary>
<exception> 1 | <remarks> | <value>
<list> | <returns> |

1. Compiler verifies syntax.

See Also

/doc (Process Documentation Comments) | XML Documentation


<c>

<c>text</c>

where:

text
The text you would like to indicate as code.

Remarks

The <c> tag gives you a way to indicate that text within a description should be marked as code. Use <code> to indicate multiple lines as code.

Compile with /doc to process documentation comments to a file.

Example

public class MyClass {
   /// <summary><c>MyMethod</c> is a method in the <c>MyClass</c> class.
   /// </summary>
   public static void MyMethod(int Int1)
{
   }

   public static void Main ()
{
   }
}

See Also

Tags for Documentation Comments


<code>

<code>content</code>

where:

content
The text you want marked as code.

Remarks

The <code> tag gives you a way to indicate multiple lines as code. Use <c> to indicate that text within a description should be marked as code.

Compile with /doc to process documentation comments to a file.

Example

See <example> for an example of using <code>.

See Also

Tags for Documentation Comments


<example>

<example>description</example>

where:

description
A description of the code sample.

Remarks

The <example> tag lets you specify an example of how to use a method or other library member. Commonly, this would involve use of the <code> tag.

Compile with /doc to process documentation comments to a file.

Example

public class MyClass
{
   /// <summary>
   /// The GetZero method.
   /// </summary>
   /// <example> This sample shows how to call the GetZero method.
   /// <code>
   ///   class MyClass
///   {
   ///      public static int Main()
///      {
   ///         return GetZero();
   ///      }
   ///   }
   /// </code>
   /// </example>
   public static int GetZero()
{
      return 0;
   }

   public static void Main ()
{
   }
}

See Also

Tags for Documentation Comments


<exception>

<exception cref="member">description</exception>

where:

cref="member"
A reference to a member or field that is available to be called from the current compilation environment. The compiler checks that the given code element exists and translates member to the canonical element name in the output XML. member must appear within double quotation marks (" ").
description
A description for the exception class.

Remarks

The <exception> tag lets you document an exception class.

Compile with /doc to process documentation comments to a file.

Example

using System;
/// <exception cref="System.Exception">This is an exception class.</exception>
class EClass : Exception
{
}

class TestClass
{
   public static void Main()
{
      try
{
      }
      catch(EClass)
{
      }
   }
}

See Also

Tags for Documentation Comments


<list>

<list type="bullet" | "number" | "table">
   <listheader>
      <term>term</term>
      <description>description</description>
   </listheader>
   <item>
      <term>term</term>
      <description>description</description>
   </item>
</list>

where:

term
A term to define, which will be defined in text.
description
Either an item in a bullet or numbered list or the definition of a term.

Remarks

The <listheader> block is used to define the heading row of either a table or definition list. When defining a table, you only need to supply an entry for term in the heading.

Each item in the list is specified with an <item> block. When creating a definition list, you will need to specify both term and text. However, for a table, bulleted list, or numbered list, you only need to supply an entry for text.

A list or table can have as many <item> blocks as needed.

Compile with /doc to process documentation comments to a file.

Example

public class MyClass
{
   /// <remarks>Here is an example of a bulleted list:
   /// <list type="bullet">
   /// <item>
   /// <description>Item 1.</description>
   /// </item>
   /// <item>
   /// <description>Item 2.</description>
   /// </item>
   /// </list>
   /// </remarks>
   public static void Main ()
{
   }
}

See Also

Tags for Documentation Comments


<para>

<para>content</para>

where:

content
The text of the paragraph.

Remarks

The <para> tag is for use inside a tag, such as <remarks> or <returns>, and lets you add structure to the text.

Compile with /doc to process documentation comments to a file.

Example

See <summary> for an example of using <para>.

See Also

Tags for Documentation Comments


<param>

<param name='name'>description</param>

where:

name
The name of a method parameter. Enclose the name in single quotation marks (' ').
description
A description for the parameter.

Remarks

The <param> tag should be used in the comment for a method declaration to describe one of the parameters for the method.

Compile with /doc to process documentation comments to a file.

Example

public class MyClass
{
   /// <param name="Int1">Used to indicate status.</param>
   public static void MyMethod(int Int1)
{
   }

   public static void Main ()
{
   }
}

See Also

Tags for Documentation Comments


<paramref>

<paramref name="name"/>

where:

name
The name of the parameter to refer to. Enclose the name in double quotation marks (" ").

Remarks

The <paramref> tag gives you a way to indicate that a word is a parameter. The XML file can be processed to format this parameter in some distinct way.

Compile with /doc to process documentation comments to a file.

Example

public class MyClass
{
   /// <remarks>MyMethod is a method in the MyClass class.
   /// The <paramref name="Int1"/> parameter takes a number.
   /// </remarks>
   public static void MyMethod(int Int1)
{
   }

   public static void Main ()
{
   }
}

See Also

Tags for Documentation Comments


<permission>

<permission cref="member">description</permission>

where:

cref="member"
A reference to a member or field that is available to be called from the current compilation environment. The compiler checks that the given code element exists and translates member to the canonical element name in the output XML. member must appear within double quotation marks (" ").
description
A description of the access to the member.

Remarks

The <permission> tag lets you document the access of a member. The System.Security.PermissionSet lets you specify access to a member.

Compile with /doc to process documentation comments to a file.

Example

using System;
class TestClass
{
   /// <permission cref="System.Security.PermissionSet">Everyone can access this method.</permission>
   public static void Test()
{
   }
   public static void Main()
{
   }
}

See Also

Tags for Documentation Comments


<remarks>

<remarks>description</remarks>

where:

description
A description of the member.

Remarks

The <remarks> tag is where you can specify overview information about a class or other type. <summary> is where you can describe the members of the type.

Compile with /doc to process documentation comments to a file.

Example

/// <remarks>
/// You may have some overview information about this class.
/// </remarks>
public class MyClass
{
   public static void Main ()
{
   }
}

See Also

Tags for Documentation Comments


<returns>

<returns>description</returns>

where:

description
A description of the return value.

Remarks

The <returns> tag should be used in the comment for a method declaration to describe the return value.

Compile with /doc to process documentation comments to a file.

Example

public class MyClass
{
   /// <returns>Returns zero.</returns>
   public static int GetZero()
{
      return 0;
   }

   public static void Main ()
{
   }
}

See Also

Tags for Documentation Comments


<see>

<see cref="member"/>

where:

cref="member"
A reference to a member or field that is available to be called from the current compilation environment. The compiler checks that the given code element exists and passes member to the element name in the output XML. member must appear within double quotation marks (" ").

Remarks

The <see> tag lets you specify a link from within text. Use <seealso> to indicate text that you might want to appear in a See Also section.

Compile with /doc to process documentation comments to a file.

Example

See <summary> for an example of using <see>.

See Also

Tags for Documentation Comments


<seealso>

<seealso cref="member"/>

where:

cref="member"
A reference to a member or field that is available to be called from the current compilation environment. The compiler checks that the given code element exists and passes member to the element name in the output XML. member must appear within double quotation marks (" ").

Remarks

The <seealso> tag lets you specify the text that you might want to appear in a See Also section. Use <see> to specify a link from within text.

Compile with /doc to process documentation comments to a file.

Example

See <summary> for an example of using <seealso>.

See Also

Tags for Documentation Comments


<summary>

<summary>description</summary>

where:

description
A summary of the object.

Remarks

The <summary> tag should be used to describe a member for a type. Use <remarks> to supply information about the type itself.

Compile with /doc to process documentation comments to a file.

Example

public class MyClass
{
   /// <summary>MyMethod is a method in the MyClass class.
   /// <para>Here's how you could make a second paragraph in a description. <see cref="System.Console.WriteLine"/> for information about output statements.</para>
   /// <seealso cref="MyClass.Main"/>
   /// </summary>
   public static void MyMethod(int Int1)
{
   }

   public static void Main ()
{
   }
}

See Also

Tags for Documentation Comments


<value>

<value>property description</value>

where:

property description
A description for the property.

Remarks

The <value> tag lets you describe a property.

Compile with /doc to process documentation comments to a file.

Example

using System;
public class Employee
{
   private string name;
   /// <value>Name accesses the value of the name data member</value>
   public string Name
{
      get
{
         return name;
      }
      set
{
         name = value;
      }
   }
}

public class MainClass
{
   public static void Main()
{
   }
}

See Also

Tags for Documentation Comments


Processing the XML File

The compiler generates an ID string for each construct in your code that is tagged to generate documentation. (For information on how to tag your code, see Tags for Documentation Comments.) The ID string uniquely identifies the construct. Programs that process the XML file can use the ID string to identify the corresponding .NET metadata/reflection item that the documentation applies to.

The XML file is not a hierarchical representation of your code, it is a flat list with a generated ID for each element.

The compiler observes the following rules when it generates the ID strings:

  • No whitespace is placed in the string.
  • The first part of the ID string identifies the kind of member being identified, via a single character followed by a colon. The following member types are used:

Character | Description
N | namespace
T | type: class, interface, struct, enum, delegate
F | field
P | property (including indexers or other indexed properties)
M | method (including such special methods as constructors, operators, and so on)
E | event
! | error string; the rest of the string provides information about the error. The C# compiler generates error information for links that cannot be resolved.

  • The second part of the string is the fully qualified name of the item, starting at the root of the namespace. The name of the item, its enclosing type(s), and namespace are separated by periods. If the name of the item itself has periods, they are replaced by the hash-sign ('#'). It is assumed that no item has an hash-sign directly in its name. For example, the fully qualified name of the String constructor would be "System.String.#ctor".
  • For properties and methods, if there are arguments to the method, the argument list enclosed in parentheses follows. If there are no arguments, no parentheses are present. The arguments are separated by commas. The encoding of each argument follows directly how it is encoded in a .NET signature:
    • Base types. Regular types (ELEMENT_TYPE_CLASS or ELEMENT_TYPE_VALUETYPE) are represented as the fully qualified name of the type.
    • Intrinsic types (for example, ELEMENT_TYPE_I4, ELEMENT_TYPE_OBJECT, ELEMENT_TYPE_STRING, ELEMENT_TYPE_TYPEDBYREF) are represented as the fully qualified name of the corresponding full type. For example, System.Int32 or System.TypedReference.
    • ELEMENT_TYPE_VOID is represented as "=VOID".
    • ELEMENT_TYPE_PTR is represented as a '*' following the modified type.
    • ELEMENT_TYPE_BYREF is represented as a '@' following the modified type.
    • ELEMENT_TYPE_PINNED is represented as a '^' following the modified type. The C# compiler never generates this.
    • ELEMENT_TYPE_CMOD_REQ is represented as a '|' and the fully qualified name of the modifier class, following the modified type. The C# compiler never generates this.
    • ELEMENT_TYPE_CMOD_OPT is represented as a '!' and the fully qualified name of the modifier class, following the modified type.
    • ELEMENT_TYPE_SZARRAY is represented as "[]" following the element type of the array.
    • ELEMENT_TYPE_GENERICARRAY is represented as "[?]" following the element type of the array. The C# compiler never generates this.
    • ELEMENT_TYPE_ARRAY is represented as [lowerbound:size,lowerbound:size] where the number of commas is the rank - 1, and the lower bounds and size of each dimension, if known, are represented in decimal. If a lower bound or size is not specified, it is simply omitted. If the lower bound and size for a particular dimension are omitted, the ':' is omitted as well. For example, a 2-dimensional array with 1 as the lower bounds and unspecified sizes is [1:,1:].
    • ELEMENT_TYPE_FNPTR is represented as "=FUNC:type(signature)", where type is the return type, and signature is the arguments of the method. If there are no arguments, the parentheses are omitted. The C# compiler never generates this.

    The following signature components are not represented because they are never used for differentiating overloaded methods:

    • calling convention
    • return type
    • ELEMENT_TYPE_SENTINEL
  • For conversion operators only (op_Implicit and op_Explicit), the return value of the method is encoded as a '~' followed by the return type, as encoded above.

Examples

The following examples show how the ID strings for a class and its members would be generated:

namespace N  // "N:N"
{
class X    // "T:N.X"
{
    X();   // "M:N.X.#ctor"
    static X(); // "M:N.X.#cctor"  a class constructor
    X(int);  // "M:N.X.#ctor(System.Int32)"
    ~X();  // "M:N.X.Finalize"   destructor's representation in metadata
    string q;  // "F:N.X.q"
    const double PI = 3.14;  // "F:N.X.PI"
    int f();  // "M:N.X.f"
    int bb(string s, ref int y, void * z); // "M:N.X.bb(System.String,System.Int32@,=VOID*)"
    int gg(short[] array1, int[,] array); // "M:N.X.gg(System.Int16[], System.Int32[0:,0:])"
    X operator+(X, X); // "M:N.X.op_Addition(N.X,N.X)"
    int prop {get; set;} // "P:N.X.prop"
    event MyDelegate d; // "E:N.X.d"
    int this[string s]; // "P:N.X.Item(System.String)"
    class Nested{} // "T:N.X.Nested"
    delegate void D(int i); // "T:N.X.D"
    explicit operator int(X x); // "M:N.X.op_Explicit(N.X)~System.Int32"
}
}

See Also

/doc (Process Documentation Comments) | XML Documentation


Accueil

Accueil CHM

Normal, dit comme ça,
vous ne risquez pas de comprendre.